Repository: SCons/scons

Issue:
Can't subst \$( in Textfile
**Describe the bug**
Generating .NET Core csproj files I needed to insert this string literal into the output
 `NETCore\bin\$(Platform)\$(Configuration)\`

But there does not seem to be any way to output the sequence `\$(`

To reproduce clone this:
   https://github.com/datalogics-robb/quote-test-scons

run `scons` to generate  GenFile.csproj and observe this string in the output:
`NETCore\bin\$Platform)\$Configuration)\`

SCons version 4.2.0
CPython 3.9.[67] tested  on Mac/Linux



Discussion:
Just capturing what was noted on Discord, although I got some details wrong there - the code uses some different conventions and it tripped me up. The `textfile` tool (which also implements `substfile`) sets up an action function which does substitution on the values in the `SUBST_DICT`, using `env.subst(value)`. With no keyword args, this defaults to `raw=0` which will end up calling `scons_subst` with `SUBST_CMD`.  But the intended usage here is just a plain textual substitution, not either the fancy "for command line" or the alternative "for signature", so this is probably not what's wanted in this context. Suggest adding `raw=1` (not `raw=True` as I said in the Discord thread).

Resolving patch:
diff --git a/SCons/Tool/textfile.py b/SCons/Tool/textfile.py
--- a/SCons/Tool/textfile.py
+++ b/SCons/Tool/textfile.py
@@ -1,6 +1,6 @@
-# -*- python -*-
+# MIT License
 #
-# __COPYRIGHT__
+# Copyright The SCons Foundation
 #
 # Permission is hereby granted, free of charge, to any person obtaining
 # a copy of this software and associated documentation files (the
@@ -20,9 +20,8 @@
 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-#
 
-__doc__ = """
+"""
 Textfile/Substfile builder for SCons.
 
     Create file 'target' which typically is a textfile.  The 'source'
@@ -44,8 +43,6 @@
     is unpredictable whether the expansion will occur.
 """
 
-__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
-
 import SCons
 
 
@@ -58,6 +55,7 @@
 
 LINESEP = '\n'
 
+
 def _do_subst(node, subs):
     """
     Fetch the node contents and replace all instances of the keys with
@@ -86,7 +84,7 @@ def _action(target, source, env):
     # prepare the line separator
     linesep = env['LINESEPARATOR']
     if linesep is None:
-        linesep = LINESEP # os.linesep
+        linesep = LINESEP  # os.linesep
     elif is_String(linesep):
         pass
     elif isinstance(linesep, Value):
@@ -114,7 +112,7 @@ def _action(target, source, env):
             if callable(value):
                 value = value()
             if is_String(value):
-                value = env.subst(value)
+                value = env.subst(value, raw=1)
             else:
                 value = str(value)
             subs.append((k, value))


Test patch:
diff --git a/test/textfile/fixture/SConstruct b/test/textfile/fixture/SConstruct
--- a/test/textfile/fixture/SConstruct
+++ b/test/textfile/fixture/SConstruct
@@ -1,6 +1,5 @@
 DefaultEnvironment(tools=[])
 
-
 env = Environment(tools=['textfile'])
 data0 = ['Goethe', 'Schiller']
 data = ['lalala', 42, data0, 'tanteratei']
@@ -10,6 +9,10 @@ env.Textfile('foo2', data, LINESEPARATOR='|*')
 env.Textfile('foo1a.txt', data + [''])
 env.Textfile('foo2a.txt', data + [''], LINESEPARATOR='|*')
 
+issue_4021_textfile = r'<HintPath>..\..\..\@HINT_PATH@\Datalogics.PDFL.dll</HintPath>'
+env.Textfile('issue-4021.txt', issue_4021_textfile,
+             SUBST_DICT={'@HINT_PATH@': r'NETCore\bin\$$(Platform)\$$(Configuration)'})
+
 # recreate the list with the data wrapped in Value()
 data0 = list(map(Value, data0))
 data = list(map(Value, data))
diff --git a/test/textfile/textfile.py b/test/textfile/textfile.py
--- a/test/textfile/textfile.py
+++ b/test/textfile/textfile.py
@@ -1,6 +1,7 @@
 #!/usr/bin/env python
+# MIT License
 #
-# __COPYRIGHT__
+# Copyright The SCons Foundation
 #
 # Permission is hereby granted, free of charge, to any person obtaining
 # a copy of this software and associated documentation files (the
@@ -20,9 +21,6 @@
 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-#
-
-__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
 import TestSCons
 
@@ -30,7 +28,7 @@
 
 test = TestSCons.TestSCons()
 
-foo1 = test.workpath('foo1.txt')
+# foo1 = test.workpath('foo1.txt')
 # foo2  = test.workpath('foo2.txt')
 # foo1a = test.workpath('foo1a.txt')
 # foo2a = test.workpath('foo2a.txt')
@@ -51,6 +49,8 @@
 foo1aText = foo1Text + linesep
 foo2aText = foo2Text + '|*'
 
+issue_4021_text = r'<HintPath>..\..\..\NETCore\bin\$(Platform)\$(Configuration)\Datalogics.PDFL.dll</HintPath>'
+
 test.up_to_date(arguments='.')
 
 files = list(map(test.workpath, (
@@ -81,6 +81,7 @@ def check_times():
 test.must_match('bar1a.txt', foo1aText, mode=match_mode)
 test.must_match('foo2a.txt', foo2aText, mode=match_mode)
 test.must_match('bar2a.txt', foo2aText, mode=match_mode)
+test.must_match('issue-4021.txt', issue_4021_text, mode=match_mode)
 check_times()
 
 # write the contents and make sure the files