@@ -4,8 +4,8 @@ use brush_parser::{
44 Parser , ParserOptions ,
55 ast:: {
66 AndOr , Assignment , AssignmentName , AssignmentValue , Command , CommandPrefix ,
7- CommandPrefixOrSuffixItem , CommandSuffix , CompoundListItem , Pipeline , Program ,
8- SeparatorOperator , SimpleCommand , SourceLocation , Word ,
7+ CommandPrefixOrSuffixItem , CommandSuffix , CompoundCommand , CompoundList , CompoundListItem ,
8+ DoGroupCommand , Pipeline , Program , SeparatorOperator , SimpleCommand , SourceLocation , Word ,
99 } ,
1010 word:: { WordPiece , WordPieceWithSource } ,
1111} ;
@@ -135,43 +135,115 @@ fn pipeline_to_command(pipeline: &Pipeline) -> Option<(TaskParsedCommand, Range<
135135 Some ( ( TaskParsedCommand { envs, program : unquote ( program) ?, args } , range) )
136136}
137137
138- fn pipeline_program_is_cd ( pipeline : & Pipeline ) -> bool {
139- let Pipeline { timed : None , bang : false , seq } = pipeline else {
138+ fn command_name_may_change_cwd ( command_name : & str ) -> bool {
139+ matches ! ( command_name, "cd" | "chdir" | "." | "source" | "eval" )
140+ }
141+
142+ fn wrapper_command_may_change_cwd ( suffix : Option < & CommandSuffix > ) -> bool {
143+ let Some ( CommandSuffix ( suffix_items) ) = suffix else {
140144 return false ;
141145 } ;
142- let [ Command :: Simple ( simple_command) ] = seq. as_slice ( ) else {
146+
147+ for suffix_item in suffix_items {
148+ let CommandPrefixOrSuffixItem :: Word ( word) = suffix_item else {
149+ continue ;
150+ } ;
151+ let Some ( arg) = unquote ( word) else {
152+ return true ;
153+ } ;
154+ if arg == "--" || arg. starts_with ( '-' ) {
155+ continue ;
156+ }
157+ return command_name_may_change_cwd ( arg. as_str ( ) ) ;
158+ }
159+
160+ false
161+ }
162+
163+ fn simple_command_may_change_cwd ( simple_command : & SimpleCommand ) -> bool {
164+ let Some ( program) = & simple_command. word_or_name else {
143165 return false ;
144166 } ;
145- let SimpleCommand { word_or_name : Some ( program) , .. } = simple_command else {
146- return false ;
167+ let Some ( program) = unquote ( program ) else {
168+ return true ;
147169 } ;
148- unquote ( program) . is_some_and ( |program| program. as_str ( ) == "cd" )
170+
171+ match program. as_str ( ) {
172+ "command" | "builtin" | "time" => {
173+ wrapper_command_may_change_cwd ( simple_command. suffix . as_ref ( ) )
174+ }
175+ command_name => command_name_may_change_cwd ( command_name) ,
176+ }
177+ }
178+
179+ fn and_or_list_may_change_cwd ( and_or_list : & brush_parser:: ast:: AndOrList ) -> bool {
180+ pipeline_may_change_cwd ( & and_or_list. first )
181+ || and_or_list. additional . iter ( ) . any ( |and_or| {
182+ let pipeline = match and_or {
183+ AndOr :: And ( pipeline) | AndOr :: Or ( pipeline) => pipeline,
184+ } ;
185+ pipeline_may_change_cwd ( pipeline)
186+ } )
187+ }
188+
189+ fn compound_list_may_change_cwd ( compound_list : & CompoundList ) -> bool {
190+ compound_list
191+ . 0
192+ . iter ( )
193+ . any ( |CompoundListItem ( and_or_list, _) | and_or_list_may_change_cwd ( and_or_list) )
194+ }
195+
196+ fn do_group_may_change_cwd ( do_group : & DoGroupCommand ) -> bool {
197+ compound_list_may_change_cwd ( & do_group. list )
198+ }
199+
200+ fn compound_command_may_change_cwd ( compound_command : & CompoundCommand ) -> bool {
201+ match compound_command {
202+ CompoundCommand :: Arithmetic ( _) | CompoundCommand :: Subshell ( _) => false ,
203+ CompoundCommand :: ArithmeticForClause ( command) => do_group_may_change_cwd ( & command. body ) ,
204+ CompoundCommand :: BraceGroup ( command) => compound_list_may_change_cwd ( & command. list ) ,
205+ CompoundCommand :: ForClause ( command) => do_group_may_change_cwd ( & command. body ) ,
206+ CompoundCommand :: CaseClause ( command) => command
207+ . cases
208+ . iter ( )
209+ . any ( |case| case. cmd . as_ref ( ) . is_some_and ( compound_list_may_change_cwd) ) ,
210+ CompoundCommand :: IfClause ( command) => {
211+ compound_list_may_change_cwd ( & command. condition )
212+ || compound_list_may_change_cwd ( & command. then )
213+ || command. elses . as_ref ( ) . is_some_and ( |elses| {
214+ elses. iter ( ) . any ( |else_clause| {
215+ else_clause. condition . as_ref ( ) . is_some_and ( compound_list_may_change_cwd)
216+ || compound_list_may_change_cwd ( & else_clause. body )
217+ } )
218+ } )
219+ }
220+ CompoundCommand :: WhileClause ( command) | CompoundCommand :: UntilClause ( command) => {
221+ compound_list_may_change_cwd ( & command. 0 ) || do_group_may_change_cwd ( & command. 1 )
222+ }
223+ }
224+ }
225+
226+ fn command_may_change_cwd ( command : & Command ) -> bool {
227+ match command {
228+ Command :: Simple ( simple_command) => simple_command_may_change_cwd ( simple_command) ,
229+ Command :: Compound ( compound_command, _) => compound_command_may_change_cwd ( compound_command) ,
230+ Command :: Function ( function) => compound_command_may_change_cwd ( & function. body . 0 ) ,
231+ Command :: ExtendedTest ( ..) => false ,
232+ }
233+ }
234+
235+ fn pipeline_may_change_cwd ( pipeline : & Pipeline ) -> bool {
236+ pipeline. seq . iter ( ) . any ( command_may_change_cwd)
149237}
150238
151239#[ must_use]
152- pub fn contains_cd_command ( cmd : & str ) -> bool {
240+ pub fn shell_command_may_change_cwd ( cmd : & str ) -> bool {
153241 let mut parser = Parser :: new ( cmd. as_bytes ( ) , & PARSER_OPTIONS ) ;
154242 let Ok ( Program { complete_commands } ) = parser. parse_program ( ) else {
155- return false ;
243+ return true ;
156244 } ;
157245
158- for compound_list in & complete_commands {
159- for CompoundListItem ( and_or_list, _) in & compound_list. 0 {
160- if pipeline_program_is_cd ( & and_or_list. first ) {
161- return true ;
162- }
163- for and_or in & and_or_list. additional {
164- let pipeline = match and_or {
165- AndOr :: And ( pipeline) | AndOr :: Or ( pipeline) => pipeline,
166- } ;
167- if pipeline_program_is_cd ( pipeline) {
168- return true ;
169- }
170- }
171- }
172- }
173-
174- false
246+ complete_commands. iter ( ) . any ( compound_list_may_change_cwd)
175247}
176248
177249#[ must_use]
@@ -202,16 +274,26 @@ mod tests {
202274 use super :: * ;
203275
204276 #[ test]
205- fn test_contains_cd_command_with_unresolved_arg ( ) {
206- assert ! ( contains_cd_command( r#"cd "$APP_DIR""# ) ) ;
207- assert ! ( contains_cd_command( r#"echo ok && cd "$APP_DIR""# ) ) ;
208- assert ! ( contains_cd_command( r#"FOO=bar 'cd' "$APP_DIR""# ) ) ;
277+ fn test_shell_command_may_change_cwd_with_unresolved_arg ( ) {
278+ assert ! ( shell_command_may_change_cwd( r#"cd "$APP_DIR""# ) ) ;
279+ assert ! ( shell_command_may_change_cwd( r#"echo ok && cd "$APP_DIR""# ) ) ;
280+ assert ! ( shell_command_may_change_cwd( r#"FOO=bar 'cd' "$APP_DIR""# ) ) ;
281+ }
282+
283+ #[ test]
284+ fn test_shell_command_may_change_cwd_in_compound_commands ( ) {
285+ assert ! ( shell_command_may_change_cwd( "if true; then cd snapshots; fi" ) ) ;
286+ assert ! ( shell_command_may_change_cwd( "{ cd snapshots; }" ) ) ;
287+ assert ! ( shell_command_may_change_cwd( "f(){ cd snapshots; }; f" ) ) ;
288+ assert ! ( shell_command_may_change_cwd( "command cd snapshots" ) ) ;
289+ assert ! ( shell_command_may_change_cwd( "time cd snapshots" ) ) ;
209290 }
210291
211292 #[ test]
212- fn test_contains_cd_command_ignores_cd_argument_text ( ) {
213- assert ! ( !contains_cd_command( r#"echo "cd $APP_DIR""# ) ) ;
214- assert ! ( !contains_cd_command( "cdtool $APP_DIR" ) ) ;
293+ fn test_shell_command_may_change_cwd_ignores_non_current_shell_cd ( ) {
294+ assert ! ( !shell_command_may_change_cwd( r#"echo "cd $APP_DIR""# ) ) ;
295+ assert ! ( !shell_command_may_change_cwd( "cdtool $APP_DIR" ) ) ;
296+ assert ! ( !shell_command_may_change_cwd( "(cd snapshots)" ) ) ;
215297 }
216298
217299 #[ test]
0 commit comments