@@ -844,14 +844,9 @@ private static bool TryConsumeComplexParamExpansion(
844844 ReadOnlySpan < char > src , int start , List < BashToken > tokens , out int afterBrace )
845845 {
846846 // src[start] = '$', src[start+1] = '{'. We need to find the matching
847- // '}' and decide: simple ${VAR} -> false (let word reader take it);
848- // ${...//...} or any other "complex" form -> emit UnparseableSentinel.
849- //
850- // For v0.1 we treat the presence of a slash inside the braces as the
851- // single signal of "complex param expansion" (per the locked
852- // interpretation #2 in the OpenSpec change). Other operators inside
853- // ${...} (like ${X-default}, ${X#prefix}) fall through to the word
854- // reader; a future PR can tighten this if needed.
847+ // '}' and decide: a simple variable, positional, or special parameter
848+ // falls through to the word reader. Operators can themselves contain
849+ // executable substitutions, so every other body fails closed.
855850 var openBrace = start + 1 ;
856851 var scan = OpaqueRegionScanner . Scan ( src , openBrace , '{' , '}' ) ;
857852 if ( ! scan . Closed )
@@ -870,20 +865,9 @@ private static bool TryConsumeComplexParamExpansion(
870865 var endInclusive = scan . EndIndex ;
871866 var bodyStart = openBrace + 1 ;
872867 var bodyEnd = endInclusive ; // exclusive of '}'
873- var hasSlash = false ;
874- for ( var k = bodyStart ; k < bodyEnd ; k ++ )
868+ var body = src . Slice ( bodyStart , bodyEnd - bodyStart ) ;
869+ if ( IsSimpleBracedParameterName ( body ) )
875870 {
876- if ( src [ k ] == '/' )
877- {
878- hasSlash = true ;
879- break ;
880- }
881- }
882-
883- if ( ! hasSlash )
884- {
885- // Simple ${VAR} (or ${X-default} etc.). Caller will fall through
886- // to the word reader and absorb it as part of a Word token.
887871 afterBrace = - 1 ;
888872 return false ;
889873 }
@@ -895,7 +879,7 @@ private static bool TryConsumeComplexParamExpansion(
895879 null ,
896880 start ,
897881 length ,
898- "complex parameter expansion '${var//pat/repl}' not supported in v0.1 " ) ) ;
882+ "complex parameter expansion is not supported in v0.3 " ) ) ;
899883 afterBrace = start + length ;
900884 return true ;
901885 }
@@ -1101,9 +1085,9 @@ private static bool TryAppendBashExpansion(
11011085
11021086 expansionLength = scan . EndIndex - start + 1 ;
11031087 name = src . Slice ( start + 2 , expansionLength - 3 ) . ToString ( ) ;
1104- if ( name . Length == 0 || name . IndexOf ( '/' ) >= 0 )
1088+ if ( ! IsSimpleBracedParameterName ( name . AsSpan ( ) ) )
11051089 {
1106- error = "complex parameter expansion '${var//pat/repl}' not supported in v0.1 " ;
1090+ error = "complex parameter expansion is not supported in v0.3 " ;
11071091 index += expansionLength ;
11081092 return true ;
11091093 }
@@ -1173,6 +1157,46 @@ private static bool IsAllAsciiDigits(string value)
11731157 return true ;
11741158 }
11751159
1160+ private static bool IsSimpleBracedParameterName ( ReadOnlySpan < char > value )
1161+ {
1162+ if ( value . Length == 0 )
1163+ {
1164+ return false ;
1165+ }
1166+
1167+ if ( value . Length == 1 &&
1168+ value [ 0 ] is '?' or '$' or '#' or '-' or '!' or '@' or '*' )
1169+ {
1170+ return true ;
1171+ }
1172+
1173+ var allDigits = true ;
1174+ for ( var index = 0 ; index < value . Length ; index ++ )
1175+ {
1176+ allDigits &= value [ index ] is >= '0' and <= '9' ;
1177+ }
1178+
1179+ if ( allDigits )
1180+ {
1181+ return true ;
1182+ }
1183+
1184+ if ( ! IsBashIdentifierStart ( value [ 0 ] ) )
1185+ {
1186+ return false ;
1187+ }
1188+
1189+ for ( var index = 1 ; index < value . Length ; index ++ )
1190+ {
1191+ if ( ! IsBashIdentifierContinuation ( value [ index ] ) )
1192+ {
1193+ return false ;
1194+ }
1195+ }
1196+
1197+ return true ;
1198+ }
1199+
11761200 private static bool IsBashIdentifierStart ( char value ) =>
11771201 value == '_' || value is >= 'A' and <= 'Z' or >= 'a' and <= 'z' ;
11781202
0 commit comments