Suggestion: provide interpreter-specific quoting for replacement tokens in dynamic folder scripts

It is currently prohibitively difficult to use replacement tokens such as $EffectiveUsername$within strings in dynamic scripts, because it cannot be guaranteed that characters that would terminate the string are not included within the replacement value.

(This is not a new problem, and has been described at length in the past, e.g. here and here – this offers a possible alternative solution.)

For example, given the PowerShell $my_username = '$EffectiveUsername$', you cannot guarantee that the effective username does not contain the quote char (such as joe.o'brien), which would render the resulting PowerShell as $my_username = 'joe.o'brien' (which fails to parse, starting at brien).

Even using here-strings (@'...'@) is fraught (as much due to idioms of PowerShell); more cases are covered, but you still cannot guarantee that the closing '@ does not appear in the replacement value.

I don’t see any reasonable way for an end-user to craft a PowerShell script which can guard against this type of replacement, at least not without some assistance from the tool doing the replacement.

My suggestion to resolve this is for there to be some supported syntax within the replacement token which indicates that RoyalTS should itself quote and/or escape the token as part of the replacement. For example, using /q (i.e. quoted) at the end of the token name: $EffectiveUsername/q$.

The exact syntax element is not important – it just needs to be something reasonable that can be reliably located by RoyalTS and wouldn’t have worked before, so existing scripts are less likely to be affected. Other candidates might be $'EffectiveUsername'$ (which may be a little esoteric), or a functional-style $Quote(EffectiveUsername)$ (assuming brackets cannot currently be part of tokens); or even just anything prefixed with ‘Quoted’, i.e. $QuotedEffectiveUsername$.

The resulting replacement would then be interpreter-specific, i.e. for each of the interpreters you support, you define the best non-interpolated string-quoting function you can, and apply its transformation to the existing replacement value.

e.g. for PowerShell, the replacement strategy is probably to escape any single-quote by doubling it, then prepend and append a single-quote, i.e. if $EffectiveUsername$ returns joe.o'brien, $EffectiveUsername/q$ would return 'joe.o''brien'. The PowerShell script we would then write in the dynamic folder definition would become $my_username = $EffectiveUsername/q$ (note how the PS script now does not provide its own quotes).

Some potential interpreter-specific approaches are:

Intepreter Strategy Example Possible C# Implementation
PowerShell duplicate squo; wrap with squo 'joe.o''brien wrote "Hello" to C:\test.txt' string.Concat("'", value.Replace("'", "''"), "'");
JSON/JavaScript duplicate bksl; replace newlines with bksl+r and/or bksl+n; replace dquo with bksl+dquo; wrap with dquo (or use the JSON Serializer in your codebase’s libraries) "joe.o'brien wrote \"Hello\" to C:\\test.txt" string.Concat("\"", value.Replace("\\", "\\\\").Replace("\r", "\\r").Replace("\n", "\\n").Replace("\"", "\\\""), "\"");
Perl/PHP/Ruby† duplicate bksl; replace squo with bksl+squo; wrap in squo 'joe.o\'brien wrote "Hello" to C:\test.txt' string.Concat("'", value.Replace("\\", "\\\\").Replace("'", "\\'"), "'");
Bash replace squo with magic sequence squo+dquo+squo+dquo+squo; wrap in squo 'joe.o'"'"'brien wrote "Hello" to C:\\test.txt' string.Concat("'", s.Replace("'", "'\"'\"'"), "'");
Python† duplicate bksl; replace newlines with bksl+r and/or bksl+n; replace squo with bksl+squo; wrap with squo (the JS version may also be suitable) 'joe.o\'brien wrote "Hello" to C:\test.txt' string.Concat("'", s.Replace("\\", "\\\\").Replace("\r", "\\r").Replace("\n", "\\n").Replace("'", "\\'"), "'");

(where bksl = \; squo = '; dquo = "; r and n are literal)

† Perl and Ruby have alternative syntax, e.g. q'...' and %q'...', but I’m not sure it adds anything here to make dedicated versions; similarly Python has r'...' but I don’t think it can escape a backslash at the end of a value, so is discounted.

A filthy, rough C# proof-of-concept (written in LINQPad) is supplied within the expandable section here...
// regex to find $Tokens$ within dynamic scripts
var tokenOptions = RegexOptions.ExplicitCapture|RegexOptions.CultureInvariant|RegexOptions.Compiled;
var tokenLocator = new Regex(@"[$](?<tok>\w+)(?<quo>[/][Qq])?[$]", tokenOptions);


// some example token values (you would use your existing token resolver)
var tokens = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) 
{
    {"EffectiveUsername", @"joe.o'brien"},
    {"EffectivePassword", @"thisIsJoe'sSecretPassword""OK""?!"},
    {"Notes", @"This is very likely
to have ""newlines"", \backslashes and 'other' nasty
characters which we should be aware of,
especially at the end of lines,\
withi
n existing words
or even 'at' ""the"" end $of the string\"}
};


// interpreter-specific quotation rules (or, use an abstract function in any base Interpreter class)
var quoters = new Dictionary<string, Func<string, string>>(StringComparer.OrdinalIgnoreCase)
{
    {string.Empty,  s => s},
    {"powershell",  s => string.Concat("'", s.Replace("'", "''"), "'") },
    {"bash",        s => string.Concat("'", s.Replace("'", "'\"'\"'"), "'") },
    {"php",         s => string.Concat("'", s.Replace("\\", "\\\\").Replace("'", "\\'"), "'") },
    {"perl",        s => string.Concat("q'", s.Replace("\\", "\\\\").Replace("'", "\\'"), "'") },
    {"ruby",        s => string.Concat("%q'", s.Replace("\\", "\\\\").Replace("'", "\\'"), "'") },
    {"python",      s => string.Concat("'", s.Replace("\\", "\\\\").Replace("\r", "\\r").Replace("\n", "\\n").Replace("'", "\\'"), "'") },
    // there is probably a more-appropriate JSONSerializer method to catch all nasties; anyway...
    {"javascript",  s => string.Concat("\"", s.Replace("\\", "\\\\").Replace("\r", "\\r").Replace("\n", "\\n").Replace("\"", "\\\""), "\"") },                    
};


// function to locate tokens within dynamic scripts and replace with their resolved value
var tokenizer = new Func<string, string, string>((script, lang) => {
    // use the right quoter implementation depending on language
    var qfunc = quoters.TryGetValue(lang, out var q) ? q : quoters[string.Empty];
    
    // use the regex to replace all instances in a single call
    return tokenLocator.Replace(script, m =>
    {
        // replace tokens.TryGetValue with whatever returns the replacement value, given its token name
        if (tokens.TryGetValue(m.Groups["tok"]?.Value, out var tokenValue))
        {
            if (m.Groups["quo"].Success)
            {
                tokenValue = qfunc.Invoke(tokenValue);
            }
            return tokenValue;
        }
        return m.Value;
    });
});


// some sample scripts with as many nasties as I can think of...
var exampleScripts = new Dictionary<string, string> {
////////////////////////////////////////////////////////////////////////////////////////////
    {"perl", @"
This is my $EffectiveUsername$.  There are many others like it but this one is mine.
If I want to use it as a string literal, I can do so like this:

   my $user = $EffectiveUsername/q$;
   my $pwd  = $EffectivePassword/q$;

See...?

Now, to see how this behaves with a multi-line value like a notes field:

Bare:
$Notes$

As a script element:
   my $notes = $Notes/q$;
   
Did that work out OK...?"},
////////////////////////////////////////////////////////////////////////////////////////////
    {"powershell", @"
This is my $EffectiveUsername$.  There are many others like it but this one is mine.
If I want to use it as a string literal, I can do so like this:

   $user = $EffectiveUsername/q$
   $pwd  = $EffectivePassword/q$

See...?

Now, to see how this behaves with a multi-line value like a notes field:

Bare:
$Notes$

As a script element:
   $notes = $Notes/q$
   
Did that work out OK...?"},
////////////////////////////////////////////////////////////////////////////////////////////
    {"javascript", @"
This is my $EffectiveUsername$.  There are many others like it but this one is mine.
If I want to use it as a string literal, I can do so like this:

   {""user"": $EffectiveUsername/q$,
    ""pwd"":  $EffectivePassword/q$}

See...?

Now, to see how this behaves with a multi-line value like a notes field:

Bare:
$Notes$

As a script element:
   var notes = $Notes/q$;
   
Did that work out OK...?"},
////////////////////////////////////////////////////////////////////////////////////////////
    {"bash", @"
This is my $EffectiveUsername$.  There are many others like it but this one is mine.
If I want to use it as a string literal, I can do so like this:

   v_user=$EffectiveUsername/q$
   v_pwd=$EffectivePassword/q$

See...?

Now, to see how this behaves with a multi-line value like a notes field:

Bare:
$Notes$

As a script element:
   v_notes=$Notes/q$
   
Did that work out OK...?"},
////////////////////////////////////////////////////////////////////////////////////////////
    {"python", @"
This is my $EffectiveUsername$.  There are many others like it but this one is mine.
If I want to use it as a string literal, I can do so like this:

   user = $EffectiveUsername/q$
   pwd = $EffectivePassword/q$

See...?

Now, to see how this behaves with a multi-line value like a notes field:

Bare:
$Notes$

As a script element:
   notes = $Notes/q$
   
Did that work out OK...?"}
////////////////////////////////////////////////////////////////////////////////////////////
};

// show the output in LINQPad
exampleScripts.Select(pair => new { 
    Lang    = pair.Key, 
    Entered = pair.Value, 
    Result  = tokenizer.Invoke(pair.Value, pair.Key)
}).Dump();

Hope this gives you some ideas…

Hi!

Thanks for the feedback.

Since we have solved this issue by providing access to token values through environment variables, it will be a lot of effort to implement the above.

May I ask, why the env variable approach wouldn’t work for you or isn’t the preferred solution?

Regards,
Stefan

I wasn’t aware of this having been implemented. It certainly isn’t obvious from the Dynamic Folder UI: the Tokens dropdown still inserts the bare-form replacement token.


If it has been implemented, then it doesn’t appear to be working. For instance, given a dynamic folder with PowerShell script content…

Get-ChildItem Env: | Out-File "${env:TEMP}\RoyalTS_DynamicFolder_EnvVars.txt"

…the resulting text file does not contain any values which have obviously >been provided by RoyalTS.

(At least as of RTS v7.4.50306 / PS v5.1.19041.6216)

(Edit) I have since found the setting which enables this.


If it has not yet been implemented, but is on the roadmap, then yes, I expect that an environment variable-based approach will be fine, subject to the following caveats:

  1. That they are correctly supplied to the process, i.e. that all possible strange values are correctly encoded for an environment block (or that the resulting supplied value is simply and reliably decodable by functions available in all supported interpreters)
  2. That they are prefixed with something that identifies them as being from RoyalTS (e.g. RoyalTS_EffectiveUsername) so as not to mask existing system variables (I’m thinking ${env:USERNAME} might be relevant)
  3. That custom properties are also exported, e.g. RoyalTS_CustomProperty_MyCustomPropertyName

(Edit) Now I have found the setting which enables this, I can see that prefix and custom properties are already implemented and encoding seems to be OK (at least in PowerShell).


Note that the maximum size of an environment block in many version of Windows is surprisingly limited and, while recent iterations may have lifted this in theory, there are still many ancillary systems that anticipate it being in the region of 32KB. Even so, I think (but have not verified) that each value may still be limited, which may be pertinent for any decent-length Notes field.

(Edit) It seems to work well enough with 800KB of Notes, so far, so I imagine this has now been sorted in recent Windows editions.

I also note that there is no concept of environment variables in JSON, since there is no programmable “interpreter” used to post-process. Does RoyalTS already perform JSON-specific encoding to the replacement token when using the JSON interpreter? If so, that’s not too dissimilar to what I was suggesting.

(Edit) Question stands – I assume no support for the JSON interpreter, but we can use another interpreter to generate the JSON, which can then handle the environment variables.

In general, my need is for RoyalTS to provide an adequately-encoded form of the token that I can retrieve from within the dynamic folder script – either approach (i.e. quoting for the interpreter vs. building a valid Win32 environment block) is entirely reasonable.

(Edit) So far, so good…

I merely figured that providing a quoted flavour might fit better into your existing codebase (since it was still just leveraging the existing string-replacement), which might make it easier to port to your other supported platforms.

(Edit) Irrelevant now, since you have done the work with environment variables.

So, editing that table of replaced tokens across multiple Dynamic Folders is an exercise in absolute torture

On the surface, the table-based approach seems user-friendly enough to edit for a single Dynamic Folder object, but it utterly sucks if you have to apply it to multiple objects.

I get why you have to specify which tokens you want to become environment variables, but I’d much rather be able to paste in a list (e.g. one per line), than have to manually select each token from a dropdown, add to list, select the next one, add to list…etc. and repeat for N dynamic folders.

And trying to standardise a bunch of Dynamic Folders using the Bulk Edit feature is no use either – it makes you start with an empty list every time you bulk-edit. So any iteration on a folder script that you then want to re-apply to other folders requires recreating all the token entries again.

Perhaps import/export buttons would be really helpful for that list editor – e.g. to export the list to the clipboard, so I can paste it into Notepad and edit it, then paste the edited list back into the editor.

If the editor-to-clipboard-to-Notepad-to-clipboard-to-editor pipeline seems a bit too painful, then perhaps having a button that lets you open the ‘raw’ list in your internal code editor would be more suitable?


The same can be said for the Custom Properties editor. Support for exporting those form fields to a simple structure (maybe JSON?) for easier editing in a text/code editor, then pasting back in would be appreciated.

I’m sorry you had to chase down the setting. It is documented though. We had to “bury” it in advanced since it has to be an opt-in. We couldn’t break existing behavior.

It also seems that you are using a lot of dynamic folders. This is certainly not something we anticipated. From our experience/data, users tend to have one or at most a hand full of dynamic folders. May I ask how many dynamic folder script you are maintaining?

No, that’s entirely my fault – I didn’t see it in the release notes, but it is there and I just missed it. Advanced is the logical place for the opt-in setting. A one-line message somewhere on the main script-editor tab might improve discoverability…

You can also supply tokens using environment variables

…but I realise there is only so much prompting you can do!


Personally, I’m up to about 30 now, but I’m also the guy that writes them for colleagues, so I have “development” folder objects too, which I export to .rdfx and distribute.

For instance, I have “template” dynamic folder objects which define all the connections that are relevant for my company’s various service products, and then make customer-specific copies of them for each of the customers I support. There are custom properties which the scripts use to toggle/customise specific connections (i.e. depending on what we sold the customer). Credentials are either generated by dynamic credentials script, or pulled directly from $EffectiveUsername$/$EffectivePassword$ on the folder object itself, where that is appropriate.

This doesn’t seem (to me) like surprising use of this feature. Maybe a bit more advanced, but well within expectation.

Up to this point, if anything changed in one of the service products, I would update the script in the “template” and copy it to clipboard, then bulk-edit my own “instances” to paste in the new script. Not the nicest workflow (but manageable).

However, introducing new custom properties to a “template” has always been the pain-point in the past. Each time, I have to…

  1. either bulk-edit the instances and manually recreate all the custom properties from a blank list, then visit each instance to re-apply the property values;
  2. or delete each existing instance and recreate them from the template, then visit each instance to re-apply the property values;
  3. or visit each existing instance and patch in new custom properties around the existing values, getting the names and types right each time.

It’s even worse if I have to walk someone else through doing this.

So for some of the more-frequently updated scripts, I have sometimes bastardised $CustomField9$ or $Notes$ to fit a JSON object, so I can distribute these changes a bit more easily. I’m not proud of that (OK, maybe a little bit), but they are easier to paste!

I can see adding environment variables presenting a similar problem.

Oh, wow! That is a huge amount of dynamic folder scripts. I can see that a migration like this can raise the frustration level. I can’t talk much about it yet but we do have something in the pipeline which will probably very helpful for a situation like this. Stay tuned, we will reveal more details soon.

In the meantime, the only thing I can think of at the moment is maybe using the PowerShell cmdlets.

I’ll look into those. Obviously, I’m at risk of scripting a script to script my script, but it may have got to that point now.

I’ll look forward to that.


In the meantime, is there any scope for the “simple” copy/paste button pair on those two lists (Environment Variables and Custom Properties), just in case your pipeline item doesn’t pan out?

(Appreciating that “simple” is, of course, entirely subjective.)

Even if it just copies/accepts the <CustomProperties/> or <DynamicFolderScriptToken/> XML fragments that would be added into the document, it would probably be more usable than manually editing via the GUI, in my case.

(Again, I’m assuming that serialising/deserialising/validating that XML are already basic operations for your codebase.)


Otherwise, are objects which are created from the first-order Template objects (the ones with the red icons) automatically updated if/when the template changes?

If so, perhaps a dedicated DynamicFolderTemplate object is the right approach?

Templates don’t work that way. The values of the templates are used when a new object (or ad hoc object) is created and from there on, they keep settings independently, like any other object.