diff --git a/applications/test/exprEntry/Make/files b/applications/test/exprEntry/Make/files
new file mode 100644
index 0000000000000000000000000000000000000000..8249208d65a9f43e2027d419586f2735fbffab0b
--- /dev/null
+++ b/applications/test/exprEntry/Make/files
@@ -0,0 +1,3 @@
+Test-exprEntry.C
+
+EXE = $(FOAM_USER_APPBIN)/Test-exprEntry
diff --git a/applications/test/exprEntry/Make/options b/applications/test/exprEntry/Make/options
new file mode 100644
index 0000000000000000000000000000000000000000..41306609f208806f0c6f42a2426867d3e10d4897
--- /dev/null
+++ b/applications/test/exprEntry/Make/options
@@ -0,0 +1 @@
+EXE_INC =
diff --git a/applications/test/exprEntry/Test-exprEntry.C b/applications/test/exprEntry/Test-exprEntry.C
new file mode 100644
index 0000000000000000000000000000000000000000..e5b76da7b70396021f687737c958f58113b2a56e
--- /dev/null
+++ b/applications/test/exprEntry/Test-exprEntry.C
@@ -0,0 +1,145 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2019 OpenCFD Ltd.
+-------------------------------------------------------------------------------
+License
+    This file is part of OpenFOAM.
+
+    OpenFOAM is free software: you can redistribute it and/or modify it
+    under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+    for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.
+
+Application
+    Test-exprEntry
+
+Description
+    Read in the given dictionaries and attempt to use exprEntry expansion
+    on any strings.
+
+Note
+   Since this is only for testing purposes, only handles simple dictionary
+   entries without attempting to descend into sub-dicts.
+
+\*---------------------------------------------------------------------------*/
+
+#include "argList.H"
+#include "IOstreams.H"
+#include "IOobject.H"
+#include "IFstream.H"
+#include "dictionary.H"
+#include "stringOps.H"
+#include "exprString.H"
+
+using namespace Foam;
+
+bool hasStrings(const primitiveEntry& e)
+{
+    for (const token& tok : e.stream())
+    {
+        if (tok.isString())
+        {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+//  Main program:
+
+int main(int argc, char *argv[])
+{
+    argList::noBanner();
+    argList::noParallel();
+    argList::addArgument("dict .. dictN");
+    argList args(argc, argv, false, true);
+
+    if (args.size() <= 1)
+    {
+        Info<< "Must supply a dictionary name!" << nl;
+    }
+
+    for (label argi=1; argi < args.size(); ++argi)
+    {
+        IOobject::writeDivider(Info);
+
+        IFstream is(args[argi]);
+
+        const dictionary dict(is);
+
+        Info<< "Input dictionary:" << dict << nl
+            << "With any expansions" << nl << endl;
+
+        for (const entry& dEntry : dict)
+        {
+            const auto* eptr = isA<primitiveEntry>(dEntry);
+            if (!eptr || !hasStrings(*eptr))
+            {
+                continue;
+            }
+
+            const primitiveEntry& e = *eptr;
+            Info<< e << endl;
+
+            for (const token& t : e.stream())
+            {
+                if (t.isString())
+                {
+                    string str(t.stringToken());
+
+                    const bool throwingErr = FatalError.throwExceptions();
+                    const bool throwingIOErr = FatalIOError.throwExceptions();
+
+                    try
+                    {
+                        // Can get an error if we have things like
+                        // ${{ ... $[...] }}
+                        Info<< "str : " << stringOps::expand(str, dict) << nl;
+                    }
+                    catch (const Foam::error& err)
+                    {
+                        Info<< err.message().c_str() << nl;
+                    }
+
+                    try
+                    {
+                        // Should not trigger any errors
+                        expressions::exprString expr(str, dict, false);
+                        Info<< "expr: " << expr << nl;
+                    }
+                    catch (const Foam::error& err)
+                    {
+                        Info<< err.message().c_str() << nl;
+                    }
+
+                    FatalError.throwExceptions(throwingErr);
+                    FatalIOError.throwExceptions(throwingIOErr);
+                    Info<< nl;
+                }
+            }
+        }
+    }
+
+    Info<< "\nEnd\n" << endl;
+
+    return 0;
+}
+
+
+// ************************************************************************* //
diff --git a/applications/test/exprEntry/testDict1 b/applications/test/exprEntry/testDict1
new file mode 100644
index 0000000000000000000000000000000000000000..06993eca2cb97867f2eaa912e21ffec73dd5c7d9
--- /dev/null
+++ b/applications/test/exprEntry/testDict1
@@ -0,0 +1,69 @@
+/*--------------------------------*- C++ -*----------------------------------*\
+| =========                 |                                                 |
+| \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox           |
+|  \\    /   O peration     | Version:  v1912                                 |
+|   \\  /    A nd           | Website:  www.openfoam.com                      |
+|    \\/     M anipulation  |                                                 |
+\*---------------------------------------------------------------------------*/
+FoamFile
+{
+    version     2.0;
+    format      ascii;
+    class       dictionary;
+    object      testDict;
+}
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+scalar1  10;
+
+scalar2  20;
+
+vector1  (1 2 3);
+
+vector2  (2 3 4);
+
+aVector  1;
+bVector  2;
+
+string1  "This is a scalar $scalar1, or $[   scalar1   ]";
+
+string2  "This is a vector $vector1, or $[vector1]";
+
+string3  "This is a vector $vector1, or $[(vector)vector1]";
+
+string3b "This is a vector ${vector1}, or $[(vector)vector1]";
+
+string4 "This is a vector ${{ 5 * 12 }} or $[(vector)vector1]";
+
+string5 "This is a vector ${{ 5 * 12 }} or $[(vector)vector1]";
+
+string8 "This is a vector ${{ 5 * 12 * $[(vector)vector1] }}";
+
+// These actually work
+string10 #{
+    Cond is ${{ ${{ sin(degToRad(4*$scalar1)) }} * $[(vector) vector${aVector}] }}
+#};
+
+// These actually work
+string10b #{
+    Cond is ${{ ${{ sin(degToRad(4*$scalar1)) }} * $[(vector) vector$bVector] }}
+#};
+
+
+// Fairly simple idea
+angle        35;
+valueExpr1   "vector(${{cos(degToRad($angle))}}, 2, 3)";
+
+
+// Slightly stranger ideas:
+
+axis1   (1 0 0);
+axis2   (0 1 0);
+axis3   (0 0 1);
+
+index   100;
+
+valueExpr2   "$[(vector) axis${{ ($index % 3) +1 }}] / ${{max(1, $index)}}";
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
diff --git a/src/OpenFOAM/expressions/exprEntry/expressionEntry.C b/src/OpenFOAM/expressions/exprEntry/expressionEntry.C
index 8d7090242f8eb7c1adb4a6008210d0377940b0f3..a350c3ed18bdc41c74aa99bb9a6bd39e07189697 100644
--- a/src/OpenFOAM/expressions/exprEntry/expressionEntry.C
+++ b/src/OpenFOAM/expressions/exprEntry/expressionEntry.C
@@ -81,70 +81,8 @@ addNamedToRunTimeSelectionTable
 
 // * * * * * * * * * * * * * * * Local Functions * * * * * * * * * * * * * * //
 
-namespace
-{
-// Same code as in stringOps.C
-
-// Acceptable values for $variable names.
-//
-// Similar to word::valid(), except we don't have the benefit of a parser
-// to filter out other unacceptable entries for us.
-//
-// Does not currently accept '/' in a variable name.
-// We would like "$file/$name" to expand as two variables.
-static inline bool validVariableChar(char c)
-{
-    return
-    (
-        std::isalnum(c)
-     || c == '.'
-     || c == ':'
-     || c == '_'
-    );
-}
-
-
-// For input string of "$variable with other" return the length of
-// the variable.
-//
-// Intentionally will not capture ':+', ':-' alterations. Use ${ .. } for that
-static inline std::string::size_type findVariableLen
-(
-    const std::string& s,
-    std::string::size_type pos,
-    const char sigil = '$'
-)
-{
-    std::string::size_type len = 0;
-
-    if (pos < s.length())
-    {
-        if (s[pos] == sigil)
-        {
-            // Skip leading '$' in the count!
-            ++pos;
-        }
-
-        for
-        (
-            auto iter = s.cbegin() + pos;
-            iter != s.cend() && validVariableChar(*iter);
-            ++iter
-        )
-        {
-            ++len;
-        }
-    }
-
-    return len;
-}
-
-} // End anonymous namespace
-
-
 namespace Foam
 {
-
 inline static const entry* getVariableOrDie
 (
     const word& name,
@@ -170,7 +108,6 @@ inline static const entry* getVariableOrDie
     return eptr;
 }
 
-
 } // End namespace Foam
 
 
@@ -200,60 +137,51 @@ Foam::exprTools::expressionEntry::New
 
 // * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
 
-Foam::expressions::exprString
-Foam::exprTools::expressionEntry::expand
+void Foam::exprTools::expressionEntry::inplaceExpand
 (
-    const std::string& orig,
+    std::string& s,
     const dictionary& dict
 )
 {
     // This is much like stringOps::inplaceExpand
     constexpr const char sigil = '$';
 
-    // Copy to a exprString, without any validation (using assign)
-    expressions::exprString s;
-    s.assign(orig);
+    // Step 1:
+    // Handle $[] special expansions first
 
     std::string::size_type varBeg = 0;
 
     while
     (
         (varBeg = s.find(sigil, varBeg)) != std::string::npos
-     // && varBeg < s.size()-1
+     && varBeg < s.size()-1
     )
     {
-        // No handling of escape characters
-
-        if (varBeg == s.size()-1)
+        if (varBeg && s[varBeg-1] == '\\')
         {
-            // Die if we ended with a '$'
-            FatalErrorInFunction
-                << "'" << sigil << "' found at end of " << s
-                << "(originally " << orig << ')' << nl
-                << exit(FatalError);
+            // Escaped character - pass through
+            ++varBeg;
+            continue;
         }
 
-        std::string::size_type varEnd = varBeg;
-        std::string::size_type delim = 0;
-
-        word castTo, varName;
-
         if (s[varBeg+1] == '[')
         {
             // An expression pattern with $[...]
 
-            varEnd = s.find(']', varBeg);
-            delim = 1;
+            std::string::size_type varEnd = s.find(']', varBeg);
+            std::string::size_type delim = 1;
 
             if (varEnd == std::string::npos)
             {
+                // Parsed '$[...' without closing ']' - error
                 FatalErrorInFunction
-                    << "No correct terminating ']' found in " << s
-                    << " (originally " << orig << ")" << nl
+                    << "No correct terminating ']' found in " << s << nl
                     << exit(FatalError);
+                break;
             }
 
             // Look for embedded (type) cast
+            word castTo, varName;
 
             const auto lparen = varBeg+2;
             if (lparen < s.size() && s[lparen] == '(')
@@ -276,8 +204,7 @@ Foam::exprTools::expressionEntry::expand
                     }
 
                     err << " substring "
-                        << s.substr(varBeg, varEnd-varBeg)
-                        << " (" << orig << ')' << nl
+                        << s.substr(varBeg, varEnd-varBeg) << nl
                         << exit(FatalError);
                 }
 
@@ -292,86 +219,63 @@ Foam::exprTools::expressionEntry::expand
                 );
             }
 
+            // Likely no spaces there, but for extra safety...
             stringOps::inplaceTrim(varName);
-        }
-        else
-        {
-            if (s[varBeg+1] == '{')
-            {
-                varEnd = s.find('}', varBeg);
-                delim = 1;
-            }
-            else
-            {
-                // Handling regular $var construct
-                varEnd += findVariableLen(s, varBeg, sigil);
-            }
 
-            if (varEnd == std::string::npos)
-            {
-                // Likely parsed '${...' without closing '}' - abort
-                break;
-            }
-            else if (varEnd == varBeg)
+            // Allow recursive plain expansion for the *variable* name.
+            // This means "$[(vector) var${index] ]" should work
+            stringOps::inplaceExpand(varName, dict);
+
+            // Length of original text to replace (incl. decorators)
+            const auto replaceLen = (varEnd - varBeg + 1);
+
+            const entry* eptr = getVariableOrDie(varName, dict);
+
+            std::string varValue;
+
+            if (castTo.empty())
             {
-                // Parsed '${}' or $badChar  - skip over or die?
-                FatalErrorInFunction
-                    << "No valid character after the $ in " << s
-                    << "(originally " << orig << ")" << endl
-                    << exit(FatalError);
+                // Serialized with spaces
+                varValue = eptr->stream().toString();
             }
             else
             {
-                // Assign - assumed to be validated with findVariableLen()
-                varName.assign
-                (
-                    s.substr(varBeg + 1 + delim, varEnd - varBeg - 2*delim)
-                );
+                varValue = expressionEntry::New(castTo)->toExpr(*eptr);
             }
-        }
 
-
-        // Length of original text to replace (incl. decorators)
-        const auto replaceLen = (varEnd - varBeg + 1);
-
-        const entry* eptr = getVariableOrDie(varName, dict);
-
-        std::string varValue;
-
-        if (castTo.empty())
-        {
-            // Serialized with spaces
-            varValue = eptr->stream().toString();
+            s.std::string::replace(varBeg, replaceLen, varValue);
+            varBeg += varValue.size();
         }
         else
         {
-            varValue = expressionEntry::New(castTo)->toExpr(*eptr);
+            ++varBeg;
         }
-
-        s.std::string::replace(varBeg, replaceLen, varValue);
-        varBeg += varValue.size();
     }
 
-    return s;
+
+    // Step 2:
+    // Handle all ${}, $var and ${{ ... }} expansions.
+    // - this is done second such that $[(vector) xyz] entries will have
+    //   been properly expanded by this stage
+
+    stringOps::inplaceExpand(s, dict);
 }
 
 
 Foam::expressions::exprString
-Foam::exprTools::expressionEntry::getExpression
+Foam::exprTools::expressionEntry::expand
 (
-    const word& name,
-    const dictionary& dict,
-    const bool stripComments
+    const std::string& orig,
+    const dictionary& dict
 )
 {
-    string str(dict.get<string>(name));
+    // Copy without validation (use assign)
+    expressions::exprString s;
+    s.assign(orig);
 
-    if (stripComments)
-    {
-        stringOps::inplaceRemoveComments(str);
-    }
+    inplaceExpand(s, dict);
 
-    return expand(str, dict);
+    return s;
 }
 
 
diff --git a/src/OpenFOAM/expressions/exprEntry/expressionEntry.H b/src/OpenFOAM/expressions/exprEntry/expressionEntry.H
index 65a83ca23797dda5a652dc57282b40e809592a0e..b42c8efd9475e7ee877f13ff07bf263423aeb2b8 100644
--- a/src/OpenFOAM/expressions/exprEntry/expressionEntry.H
+++ b/src/OpenFOAM/expressions/exprEntry/expressionEntry.H
@@ -134,20 +134,18 @@ public:
         //- Generic concatenate tokens to space-separated string.
         inline static string evaluate(const entry& e);
 
-        //- Expand expression with dictionary entries
-        static expressions::exprString expand
+        //- Inplace expand expression with dictionary entries
+        static void inplaceExpand
         (
-            const std::string& str,
+            std::string& s,
             const dictionary& dict
         );
 
-        //- Get and expand expression with dictionary entries,
-        //- and strip C/C++ comments from the input
-        static expressions::exprString getExpression
+        //- Expand expression with dictionary entries
+        static expressions::exprString expand
         (
-            const word& name,
-            const dictionary& dict,
-            const bool stripComments = false
+            const std::string& str,
+            const dictionary& dict
         );
 
 
diff --git a/src/OpenFOAM/expressions/exprString/exprString.C b/src/OpenFOAM/expressions/exprString/exprString.C
index 32d0cf6506aea2a32a209daa764d4bfcd36714f0..2d6bd03bc094df7c86cba4e7d3d53ab42d31b599 100644
--- a/src/OpenFOAM/expressions/exprString/exprString.C
+++ b/src/OpenFOAM/expressions/exprString/exprString.C
@@ -29,31 +29,58 @@ License
 #include "stringOps.H"
 #include "expressionEntry.H"
 
-// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+// * * * * * * * * * * * * * Static Member Functions * * * * * * * * * * * * //
 
-Foam::expressions::exprString&
-Foam::expressions::exprString::expand
+void Foam::expressions::exprString::inplaceExpand
 (
+    std::string& str,
     const dictionary& dict,
     const bool stripComments
 )
 {
     if (stripComments)
     {
-        stringOps::inplaceRemoveComments(*this);
+        stringOps::inplaceRemoveComments(str);
     }
 
-    // Not quite as efficient as it could be, but wish to have a copy
-    // of the original input for the sake of reporting errors
+    exprTools::expressionEntry::inplaceExpand(str, dict);
+}
 
-    if (std::string::npos != find('$'))
-    {
-        (*this) = exprTools::expressionEntry::expand(*this, dict);
 
-        #ifdef FULLDEBUG
-        (void)valid();
-        #endif
-    }
+Foam::expressions::exprString
+Foam::expressions::exprString::getExpression
+(
+    const word& name,
+    const dictionary& dict,
+    const bool stripComments
+)
+{
+    string orig(dict.get<string>(name));
+
+    // No validation
+    expressions::exprString expr;
+    expr.assign(std::move(orig));
+
+    inplaceExpand(expr, dict, stripComments);
+
+    return expr;
+}
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+Foam::expressions::exprString&
+Foam::expressions::exprString::expand
+(
+    const dictionary& dict,
+    const bool stripComments
+)
+{
+    inplaceExpand(*this, dict, stripComments);
+
+    #ifdef FULLDEBUG
+    (void)valid();
+    #endif
 
     return *this;
 }
diff --git a/src/OpenFOAM/expressions/exprString/exprString.H b/src/OpenFOAM/expressions/exprString/exprString.H
index 8d8faf857e961aaf8e305665d98fd7b9d64e8d89..612022d0fed47d80507f61e49e071377f6a7dd4a 100644
--- a/src/OpenFOAM/expressions/exprString/exprString.H
+++ b/src/OpenFOAM/expressions/exprString/exprString.H
@@ -115,7 +115,25 @@ public:
     ~exprString() = default;
 
 
-    // Member Functions
+    // Static Member Functions
+
+        //- Inplace expansion with dictionary variables,
+        //- and strip C/C++ comments from the input
+        static void inplaceExpand
+        (
+            std::string& str,
+            const dictionary& dict,
+            const bool stripComments = true
+        );
+
+        //- Get and expand expression with dictionary entries,
+        //- optionally strip C/C++ comments from the input
+        static exprString getExpression
+        (
+            const word& name,
+            const dictionary& dict,
+            const bool stripComments = false
+        );
 
         //- Copy convert string to exprString.
         //  No expansions, know what you are doing.
@@ -125,6 +143,9 @@ public:
         //  No expansions, know what you are doing.
         inline static exprString toExpr(std::string&& str);
 
+
+    // Member Functions
+
         //- Inplace expansion with dictionary variables,
         //- and strip C/C++ comments from the input
         exprString& expand