@@ -186,29 +186,55 @@ def partialize(
186
186
def bash (request ) -> pexpect .spawn :
187
187
188
188
logfile = None
189
+ histfile = None
190
+ tmpdir = None
191
+ bash = None
192
+
189
193
if os .environ .get ("BASHCOMP_TEST_LOGFILE" ):
190
194
logfile = open (os .environ ["BASHCOMP_TEST_LOGFILE" ], "w" )
191
195
elif os .environ .get ("CI" ):
192
196
logfile = sys .stdout
197
+
193
198
testdir = os .path .abspath (
194
199
os .path .join (os .path .dirname (__file__ ), os .pardir )
195
200
)
196
- env = os .environ .copy ()
197
- env .update (
198
- dict (
199
- SRCDIR = testdir , # TODO needed at least by bashrc
200
- SRCDIRABS = testdir ,
201
- PS1 = PS1 ,
202
- INPUTRC = "%s/config/inputrc" % testdir ,
203
- TERM = "dumb" ,
204
- LC_COLLATE = "C" , # to match Python's default locale unaware sort
205
- HISTFILE = "/dev/null" , # to leave user's history file alone
206
- )
201
+
202
+ # Create an empty temporary file for HISTFILE.
203
+ #
204
+ # To prevent the tested Bash processes from writing to the user's
205
+ # history file or any other files, we prepare an empty temporary
206
+ # file for each test.
207
+ #
208
+ # - Note that HISTFILE=/dev/null may not work. It results in the
209
+ # removal of the device /dev/null and the creation of a regular
210
+ # file at /dev/null when the number of commands reach
211
+ # HISTFILESIZE by Bash-4.3's bug. This causes the execution of
212
+ # garbages through BASH_COMPLETION_USER_FILE=/dev/null. - Note
213
+ # also that "unset -v HISTFILE" in "test/config/bashrc" was not
214
+ # adopted because "test/config/bashrc" is loaded after the
215
+ # history is read from the history file.
216
+ #
217
+ histfile = tempfile .NamedTemporaryFile (
218
+ prefix = "bash-completion-test_" , delete = False
207
219
)
208
220
209
- tmpdir = None
210
- bash = None
211
221
try :
222
+ # release the file handle so that Bash can open the file.
223
+ histfile .close ()
224
+
225
+ env = os .environ .copy ()
226
+ env .update (
227
+ dict (
228
+ SRCDIR = testdir , # TODO needed at least by bashrc
229
+ SRCDIRABS = testdir ,
230
+ PS1 = PS1 ,
231
+ INPUTRC = "%s/config/inputrc" % testdir ,
232
+ TERM = "dumb" ,
233
+ LC_COLLATE = "C" , # to match Python's default locale unaware sort
234
+ HISTFILE = histfile .name ,
235
+ )
236
+ )
237
+
212
238
marker = request .node .get_closest_marker ("bashcomp" )
213
239
214
240
# Set up the current working directory
@@ -306,6 +332,11 @@ def bash(request) -> pexpect.spawn:
306
332
bash .close ()
307
333
if tmpdir :
308
334
tmpdir .cleanup ()
335
+ if histfile :
336
+ try :
337
+ os .remove (histfile .name )
338
+ except OSError :
339
+ pass
309
340
if logfile and logfile != sys .stdout :
310
341
logfile .close ()
311
342
@@ -399,13 +430,21 @@ def assert_bash_exec(
399
430
400
431
401
432
class bash_env_saved :
433
+ counter : int = 0
434
+
402
435
def __init__ (self , bash : pexpect .spawn , sendintr : bool = False ):
436
+ bash_env_saved .counter += 1
437
+ self .prefix : str = "_BASHCOMP_TEST%d" % bash_env_saved .counter
438
+
403
439
self .bash = bash
404
- self .cwd : Optional [ str ] = None
440
+ self .cwd_changed : bool = False
405
441
self .saved_shopt : Dict [str , int ] = {}
406
442
self .saved_variables : Dict [str , int ] = {}
407
443
self .sendintr = sendintr
408
444
445
+ self .noexcept : bool = False
446
+ self .captured_error : Optional [Exception ] = None
447
+
409
448
def __enter__ (self ):
410
449
return self
411
450
@@ -418,111 +457,158 @@ def __exit__(
418
457
self ._restore_env ()
419
458
return None
420
459
460
+ def _safe_sendintr (self ):
461
+ try :
462
+ self .bash .sendintr ()
463
+ self .bash .expect_exact (PS1 )
464
+ except Exception as e :
465
+ if self .noexcept :
466
+ self .captured_error = e
467
+ else :
468
+ raise
469
+
470
+ def _safe_exec (self , cmd : str ):
471
+ try :
472
+ self .bash .sendline (cmd )
473
+ self .bash .expect_exact (cmd )
474
+ self .bash .expect_exact ("\r \n " + PS1 )
475
+ except Exception as e :
476
+ if self .noexcept :
477
+ self ._safe_sendintr ()
478
+ self .captured_error = e
479
+ else :
480
+ raise
481
+
482
+ def _safe_assert (self , cmd : str ):
483
+ try :
484
+ assert_bash_exec (self .bash , cmd , want_output = None )
485
+ except Exception as e :
486
+ if self .noexcept :
487
+ self ._safe_sendintr ()
488
+ self .captured_error = e
489
+ else :
490
+ raise
491
+
421
492
def _copy_variable (self , src_var : str , dst_var : str ):
422
- assert_bash_exec (
423
- self .bash ,
493
+ self ._safe_exec (
424
494
"if [[ ${%s+set} ]]; then %s=${%s}; else unset -v %s; fi"
425
495
% (src_var , dst_var , src_var , dst_var ),
426
496
)
427
497
428
498
def _unset_variable (self , varname : str ):
429
- assert_bash_exec ( self .bash , "unset -v %s" % varname )
499
+ self ._safe_exec ( "unset -v %s" % varname )
430
500
431
501
def _save_cwd (self ):
432
- if not self .cwd :
433
- self .cwd = self .bash .cwd
502
+ if not self .cwd_changed :
503
+ self .cwd_changed = True
504
+ self ._copy_variable ("PWD" , "%s_OLDPWD" % self .prefix )
434
505
435
506
def _check_shopt (self , name : str ):
436
- assert_bash_exec (
437
- self .bash ,
438
- '[[ $(shopt -p %s) == "${_BASHCOMP_TEST_NEWSHOPT_%s}" ]]'
439
- % (name , name ),
507
+ self ._safe_assert (
508
+ '[[ $(shopt -p %s) == "${%s_NEWSHOPT_%s}" ]]'
509
+ % (name , self .prefix , name ),
440
510
)
441
511
442
512
def _unprotect_shopt (self , name : str ):
443
513
if name not in self .saved_shopt :
444
514
self .saved_shopt [name ] = 1
445
- assert_bash_exec (
446
- self .bash ,
447
- "_BASHCOMP_TEST_OLDSHOPT_%s=$(shopt -p %s; true)"
448
- % (name , name ),
515
+ self ._safe_exec (
516
+ "%s_OLDSHOPT_%s=$(shopt -p %s || true)"
517
+ % (self .prefix , name , name ),
449
518
)
450
519
else :
451
520
self ._check_shopt (name )
452
521
453
522
def _protect_shopt (self , name : str ):
454
- assert_bash_exec (
455
- self . bash ,
456
- "_BASHCOMP_TEST_NEWSHOPT_%s=$(shopt -p %s; true)" % ( name , name ),
523
+ self . _safe_exec (
524
+ "%s_NEWSHOPT_%s=$(shopt -p %s || true)"
525
+ % ( self . prefix , name , name ),
457
526
)
458
527
459
528
def _check_variable (self , varname : str ):
460
- assert_bash_exec (
461
- self .bash ,
462
- '[[ ${%s-%s} == "${_BASHCOMP_TEST_NEWVAR_%s-%s}" ]]'
463
- % (varname , MAGIC_MARK2 , varname , MAGIC_MARK2 ),
464
- )
529
+ try :
530
+ self ._safe_assert (
531
+ '[[ ${%s-%s} == "${%s_NEWVAR_%s-%s}" ]]'
532
+ % (varname , MAGIC_MARK2 , self .prefix , varname , MAGIC_MARK2 ),
533
+ )
534
+ except Exception :
535
+ self ._copy_variable (
536
+ "%s_NEWVAR_%s" % (self .prefix , varname ), varname
537
+ )
538
+ raise
539
+ else :
540
+ if self .noexcept and self .captured_error :
541
+ self ._copy_variable (
542
+ "%s_NEWVAR_%s" % (self .prefix , varname ), varname
543
+ )
465
544
466
545
def _unprotect_variable (self , varname : str ):
467
546
if varname not in self .saved_variables :
468
547
self .saved_variables [varname ] = 1
469
- self ._copy_variable (varname , "_BASHCOMP_TEST_OLDVAR_" + varname )
548
+ self ._copy_variable (
549
+ varname , "%s_OLDVAR_%s" % (self .prefix , varname )
550
+ )
470
551
else :
471
552
self ._check_variable (varname )
472
553
473
554
def _protect_variable (self , varname : str ):
474
- self ._copy_variable (varname , "_BASHCOMP_TEST_NEWVAR_" + varname )
555
+ self ._copy_variable (varname , "%s_NEWVAR_%s" % ( self . prefix , varname ) )
475
556
476
557
def _restore_env (self ):
558
+ self .noexcept = True
559
+
477
560
if self .sendintr :
478
- self .bash .sendintr ()
479
- self .bash .expect_exact (PS1 )
561
+ self ._safe_sendintr ()
480
562
481
563
# We first go back to the original directory before restoring
482
564
# variables because "cd" affects "OLDPWD".
483
- if self .cwd :
565
+ if self .cwd_changed :
484
566
self ._unprotect_variable ("OLDPWD" )
485
- assert_bash_exec (
486
- self .bash , "command cd -- %s" % shlex .quote (str (self .cwd ))
487
- )
567
+ self ._safe_exec ('command cd -- "$%s_OLDPWD"' % self .prefix )
488
568
self ._protect_variable ("OLDPWD" )
489
- self .cwd = None
569
+ self ._unset_variable ("%s_OLDPWD" % self .prefix )
570
+ self .cwd_changed = False
490
571
491
572
for name in self .saved_shopt :
492
573
self ._check_shopt (name )
493
- assert_bash_exec (
494
- self .bash , 'eval "$_BASHCOMP_TEST_OLDSHOPT_%s"' % name
495
- )
496
- self ._unset_variable ("_BASHCOMP_TEST_OLDSHOPT_" + name )
497
- self ._unset_variable ("_BASHCOMP_TEST_NEWSHOPT_" + name )
574
+ self ._safe_exec ('eval "$%s_OLDSHOPT_%s"' % (self .prefix , name ))
575
+ self ._unset_variable ("%s_OLDSHOPT_%s" % (self .prefix , name ))
576
+ self ._unset_variable ("%s_NEWSHOPT_%s" % (self .prefix , name ))
498
577
self .saved_shopt = {}
499
578
500
579
for varname in self .saved_variables :
501
580
self ._check_variable (varname )
502
- self ._copy_variable ("_BASHCOMP_TEST_OLDVAR_" + varname , varname )
503
- self ._unset_variable ("_BASHCOMP_TEST_OLDVAR_" + varname )
504
- self ._unset_variable ("_BASHCOMP_TEST_NEWVAR_" + varname )
581
+ self ._copy_variable (
582
+ "%s_OLDVAR_%s" % (self .prefix , varname ), varname
583
+ )
584
+ self ._unset_variable ("%s_OLDVAR_%s" % (self .prefix , varname ))
585
+ self ._unset_variable ("%s_NEWVAR_%s" % (self .prefix , varname ))
505
586
self .saved_variables = {}
506
587
588
+ self .noexcept = False
589
+ if self .captured_error :
590
+ raise self .captured_error
591
+
507
592
def chdir (self , path : str ):
508
593
self ._save_cwd ()
594
+ self .cwd_changed = True
509
595
self ._unprotect_variable ("OLDPWD" )
510
- assert_bash_exec ( self .bash , "command cd -- %s" % shlex .quote (path ))
596
+ self ._safe_exec ( "command cd -- %s" % shlex .quote (path ))
511
597
self ._protect_variable ("OLDPWD" )
512
598
513
599
def shopt (self , name : str , value : bool ):
514
600
self ._unprotect_shopt (name )
515
601
if value :
516
- assert_bash_exec ( self .bash , "shopt -s %s" % name )
602
+ self ._safe_exec ( "shopt -s %s" % name )
517
603
else :
518
- assert_bash_exec ( self .bash , "shopt -u %s" % name )
604
+ self ._safe_exec ( "shopt -u %s" % name )
519
605
self ._protect_shopt (name )
520
606
521
607
def write_variable (self , varname : str , new_value : str , quote : bool = True ):
522
608
if quote :
523
609
new_value = shlex .quote (new_value )
524
610
self ._unprotect_variable (varname )
525
- assert_bash_exec ( self .bash , "%s=%s" % (varname , new_value ))
611
+ self ._safe_exec ( "%s=%s" % (varname , new_value ))
526
612
self ._protect_variable (varname )
527
613
528
614
# TODO: We may restore the "export" attribute as well though it is
@@ -531,7 +617,7 @@ def write_env(self, envname: str, new_value: str, quote: bool = True):
531
617
if quote :
532
618
new_value = shlex .quote (new_value )
533
619
self ._unprotect_variable (envname )
534
- assert_bash_exec ( self .bash , "export %s=%s" % (envname , new_value ))
620
+ self ._safe_exec ( "export %s=%s" % (envname , new_value ))
535
621
self ._protect_variable (envname )
536
622
537
623
@@ -559,7 +645,7 @@ def diff_env(before: List[str], after: List[str], ignore: str):
559
645
if not re .search (r"^(---|\+\+\+|@@ )" , x )
560
646
# Ignore variables expected to change:
561
647
and not re .search (
562
- r"^[-+](_|PPID|BASH_REMATCH|_BASHCOMP_TEST_ \w+)=" ,
648
+ r"^[-+](_|PPID|BASH_REMATCH|_BASHCOMP_TEST \w+)=" ,
563
649
x ,
564
650
re .ASCII ,
565
651
)
0 commit comments