@@ -225,3 +225,350 @@ def test_create_api_env_script_with_normal_key():
225225 finally :
226226 script_path .unlink ()
227227
228+
229+ def _shell_available (shell : str ) -> bool :
230+ """Check if a shell is available on the system"""
231+ try :
232+ result = subprocess .run (
233+ ['which' , shell ],
234+ capture_output = True ,
235+ timeout = 2
236+ )
237+ return result .returncode == 0
238+ except (subprocess .TimeoutExpired , FileNotFoundError ):
239+ return False
240+
241+
242+ def test_create_api_env_script_with_special_characters_fish ():
243+ """
244+ Test that API keys with special characters work in fish shell scripts.
245+
246+ This test verifies that shlex.quote() works correctly with fish shell.
247+ Fish is not POSIX-compliant, so there may be edge cases where POSIX-style
248+ quoting doesn't work as expected.
249+ """
250+ if not _shell_available ('fish' ):
251+ pytest .skip ("fish shell not available" )
252+
253+ test_keys = {
254+ 'GEMINI_API_KEY' : 'AIzaSyAbCdEf123456$var"quote\' backtick\\ slash'
255+ }
256+
257+ script_content = create_api_env_script (test_keys , 'fish' )
258+
259+ with tempfile .NamedTemporaryFile (mode = 'w' , suffix = '.fish' , delete = False ) as f :
260+ f .write (script_content )
261+ script_path = Path (f .name )
262+
263+ try :
264+ # Fish doesn't have a -n syntax check flag like bash/zsh
265+ # So we'll try to source it and see if it works
266+ result = subprocess .run (
267+ ['fish' , '-c' , f'source { script_path } ; exit 0' ],
268+ capture_output = True ,
269+ text = True ,
270+ timeout = 5
271+ )
272+
273+ assert result .returncode == 0 , (
274+ f"Generated fish script has syntax/execution errors: { result .stderr } \n "
275+ f"Script content:\n { script_content } "
276+ )
277+ finally :
278+ script_path .unlink ()
279+
280+
281+ def test_create_api_env_script_preserves_key_value_fish ():
282+ """
283+ Test that fish shell correctly preserves key values with special characters.
284+
285+ This is critical because fish has different quoting rules than POSIX shells,
286+ and shlex.quote() may not handle all cases correctly.
287+ """
288+ if not _shell_available ('fish' ):
289+ pytest .skip ("fish shell not available" )
290+
291+ original_key = 'AIzaSyAbCdEf123456$var"quote\' backtick\\ slash'
292+ test_keys = {
293+ 'GEMINI_API_KEY' : original_key
294+ }
295+
296+ script_content = create_api_env_script (test_keys , 'fish' )
297+
298+ with tempfile .NamedTemporaryFile (mode = 'w' , suffix = '.fish' , delete = False ) as f :
299+ f .write (script_content )
300+ script_path = Path (f .name )
301+
302+ try :
303+ # Source the script and extract the value using fish
304+ result = subprocess .run (
305+ ['fish' , '-c' , f'source { script_path } ; python3 -c "import os; print(os.environ.get(\' GEMINI_API_KEY\' , \' \' ))"' ],
306+ capture_output = True ,
307+ text = True ,
308+ timeout = 5
309+ )
310+
311+ assert result .returncode == 0 , (
312+ f"Failed to source fish script and read env var: { result .stderr } \n "
313+ f"Script content:\n { script_content } "
314+ )
315+
316+ extracted_key = result .stdout .strip ()
317+ assert extracted_key == original_key , (
318+ f"Key value was corrupted during escaping in fish shell.\n "
319+ f"Original: { repr (original_key )} \n "
320+ f"Extracted: { repr (extracted_key )} \n "
321+ f"Script content:\n { script_content } \n "
322+ f"This indicates shlex.quote() may not work correctly with fish shell."
323+ )
324+ finally :
325+ script_path .unlink ()
326+
327+
328+ def test_create_api_env_script_with_special_characters_csh ():
329+ """
330+ Test that API keys with special characters work in csh/tcsh shell scripts.
331+
332+ WARNING: csh/tcsh have fundamentally different quoting rules than POSIX shells.
333+ shlex.quote() uses POSIX single-quote syntax which may not work correctly
334+ in csh/tcsh, especially with:
335+ - Variables containing $ (variable expansion still occurs in single quotes)
336+ - Complex backslash sequences
337+ - Certain special characters
338+
339+ This test will help identify if shlex.quote() works correctly with csh/tcsh.
340+ """
341+ # Try csh first, then tcsh
342+ shell_cmd = None
343+ shell_name = None
344+ for shell in ['csh' , 'tcsh' ]:
345+ if _shell_available (shell ):
346+ shell_cmd = shell
347+ shell_name = shell
348+ break
349+
350+ if not shell_cmd :
351+ pytest .skip ("csh/tcsh not available" )
352+
353+ test_keys = {
354+ 'GEMINI_API_KEY' : 'AIzaSyAbCdEf123456$var"quote\' backtick\\ slash'
355+ }
356+
357+ script_content = create_api_env_script (test_keys , shell_name )
358+
359+ with tempfile .NamedTemporaryFile (mode = 'w' , suffix = '.csh' , delete = False ) as f :
360+ f .write (script_content )
361+ script_path = Path (f .name )
362+
363+ try :
364+ # csh/tcsh don't have a -n flag, so we'll try to source it
365+ # Use -f to prevent reading .cshrc/.tcshrc which might interfere
366+ result = subprocess .run (
367+ [shell_cmd , '-f' , '-c' , f'source { script_path } ; exit 0' ],
368+ capture_output = True ,
369+ text = True ,
370+ timeout = 5
371+ )
372+
373+ assert result .returncode == 0 , (
374+ f"Generated { shell_name } script has syntax/execution errors: { result .stderr } \n "
375+ f"Script content:\n { script_content } \n "
376+ f"This may indicate that shlex.quote() doesn't work correctly with { shell_name } ."
377+ )
378+ finally :
379+ script_path .unlink ()
380+
381+
382+ def test_create_api_env_script_preserves_key_value_csh ():
383+ """
384+ Test that csh/tcsh correctly preserves key values with special characters.
385+
386+ This is critical because csh/tcsh have fundamentally different quoting rules:
387+ - Single quotes in csh do NOT prevent variable expansion ($var still expands)
388+ - Backslash escaping works differently
389+ - The quoting mechanism is incompatible with POSIX
390+
391+ This test will likely reveal issues with using shlex.quote() for csh/tcsh.
392+ """
393+ # Try csh first, then tcsh
394+ shell_cmd = None
395+ shell_name = None
396+ for shell in ['csh' , 'tcsh' ]:
397+ if _shell_available (shell ):
398+ shell_cmd = shell
399+ shell_name = shell
400+ break
401+
402+ if not shell_cmd :
403+ pytest .skip ("csh/tcsh not available" )
404+
405+ original_key = 'AIzaSyAbCdEf123456$var"quote\' backtick\\ slash'
406+ test_keys = {
407+ 'GEMINI_API_KEY' : original_key
408+ }
409+
410+ script_content = create_api_env_script (test_keys , shell_name )
411+
412+ with tempfile .NamedTemporaryFile (mode = 'w' , suffix = '.csh' , delete = False ) as f :
413+ f .write (script_content )
414+ script_path = Path (f .name )
415+
416+ try :
417+ # Source the script and extract the value using csh/tcsh
418+ # Use -f to prevent reading .cshrc/.tcshrc
419+ result = subprocess .run (
420+ [shell_cmd , '-f' , '-c' , f'source { script_path } ; python3 -c "import os; print(os.environ.get(\' GEMINI_API_KEY\' , \' \' ))"' ],
421+ capture_output = True ,
422+ text = True ,
423+ timeout = 5
424+ )
425+
426+ assert result .returncode == 0 , (
427+ f"Failed to source { shell_name } script and read env var: { result .stderr } \n "
428+ f"Script content:\n { script_content } "
429+ )
430+
431+ extracted_key = result .stdout .strip ()
432+ assert extracted_key == original_key , (
433+ f"Key value was corrupted during escaping in { shell_name } shell.\n "
434+ f"Original: { repr (original_key )} \n "
435+ f"Extracted: { repr (extracted_key )} \n "
436+ f"Script content:\n { script_content } \n "
437+ f"This indicates shlex.quote() does NOT work correctly with { shell_name } .\n "
438+ f"csh/tcsh have different quoting rules than POSIX shells."
439+ )
440+ finally :
441+ script_path .unlink ()
442+
443+
444+ def test_create_api_env_script_csh_variable_expansion_issue ():
445+ """
446+ Test a specific csh/tcsh issue: variable expansion in single quotes.
447+
448+ In csh/tcsh, single quotes do NOT prevent variable expansion.
449+ This means a key containing $HOME will expand to the actual home directory
450+ path, which is incorrect behavior.
451+
452+ This test demonstrates the fundamental incompatibility between
453+ POSIX-style quoting (shlex.quote) and csh/tcsh.
454+ """
455+ # Try csh first, then tcsh
456+ shell_cmd = None
457+ shell_name = None
458+ for shell in ['csh' , 'tcsh' ]:
459+ if _shell_available (shell ):
460+ shell_cmd = shell
461+ shell_name = shell
462+ break
463+
464+ if not shell_cmd :
465+ pytest .skip ("csh/tcsh not available" )
466+
467+ # Create a key that contains $HOME to test variable expansion
468+ # In POSIX shells, this should be preserved as-is
469+ # In csh/tcsh, this might expand to the actual home directory
470+ test_key = 'api_key_with_$HOME_in_it'
471+ test_keys = {
472+ 'GEMINI_API_KEY' : test_key
473+ }
474+
475+ script_content = create_api_env_script (test_keys , shell_name )
476+
477+ with tempfile .NamedTemporaryFile (mode = 'w' , suffix = '.csh' , delete = False ) as f :
478+ f .write (script_content )
479+ script_path = Path (f .name )
480+
481+ try :
482+ # Source the script and extract the value
483+ result = subprocess .run (
484+ [shell_cmd , '-f' , '-c' , f'source { script_path } ; python3 -c "import os; print(os.environ.get(\' GEMINI_API_KEY\' , \' \' ))"' ],
485+ capture_output = True ,
486+ text = True ,
487+ timeout = 5
488+ )
489+
490+ assert result .returncode == 0 , (
491+ f"Failed to source { shell_name } script: { result .stderr } \n "
492+ f"Script content:\n { script_content } "
493+ )
494+
495+ extracted_key = result .stdout .strip ()
496+ # This test will likely FAIL, demonstrating the issue
497+ assert extracted_key == test_key , (
498+ f"Variable expansion occurred in { shell_name } despite single quotes!\n "
499+ f"Expected: { repr (test_key )} \n "
500+ f"Got: { repr (extracted_key )} \n "
501+ f"Script content:\n { script_content } \n "
502+ f"This proves that shlex.quote() (POSIX single quotes) does NOT work\n "
503+ f"correctly with csh/tcsh, which expand variables even in single quotes."
504+ )
505+ finally :
506+ script_path .unlink ()
507+
508+
509+ def test_create_api_env_script_fish_edge_cases ():
510+ """
511+ Test fish shell with various edge cases that might reveal quoting issues.
512+
513+ Fish shell, while often compatible with POSIX-style quoting, may have
514+ edge cases with certain character combinations.
515+ """
516+ if not _shell_available ('fish' ):
517+ pytest .skip ("fish shell not available" )
518+
519+ edge_cases = [
520+ 'key with spaces' ,
521+ "key'with'single'quotes" ,
522+ 'key"with"double"quotes' ,
523+ 'key$with$dollars' ,
524+ 'key\\ with\\ backslashes' ,
525+ 'key`with`backticks' ,
526+ 'key(with)parentheses' ,
527+ 'key[with]brackets' ,
528+ 'key{with}braces' ,
529+ 'key;with;semicolons' ,
530+ 'key|with|pipes' ,
531+ 'key&with&ersands' ,
532+ 'key<with>redirects' ,
533+ 'key\n with\n newlines' ,
534+ 'key\t with\t tabs' ,
535+ ]
536+
537+ for i , test_key in enumerate (edge_cases ):
538+ test_keys = {
539+ 'TEST_API_KEY' : test_key
540+ }
541+
542+ script_content = create_api_env_script (test_keys , 'fish' )
543+
544+ with tempfile .NamedTemporaryFile (mode = 'w' , suffix = f'.fish' , delete = False ) as f :
545+ f .write (script_content )
546+ script_path = Path (f .name )
547+
548+ try :
549+ # Try to source it
550+ result = subprocess .run (
551+ ['fish' , '-c' , f'source { script_path } ; python3 -c "import os; print(os.environ.get(\' TEST_API_KEY\' , \' \' ))"' ],
552+ capture_output = True ,
553+ text = True ,
554+ timeout = 5
555+ )
556+
557+ if result .returncode != 0 :
558+ pytest .fail (
559+ f"Fish shell failed with edge case { i + 1 } : { repr (test_key )} \n "
560+ f"Error: { result .stderr } \n "
561+ f"Script content:\n { script_content } "
562+ )
563+
564+ extracted_key = result .stdout .strip ()
565+ if extracted_key != test_key :
566+ pytest .fail (
567+ f"Fish shell corrupted value for edge case { i + 1 } : { repr (test_key )} \n "
568+ f"Expected: { repr (test_key )} \n "
569+ f"Got: { repr (extracted_key )} \n "
570+ f"Script content:\n { script_content } "
571+ )
572+ finally :
573+ script_path .unlink ()
574+
0 commit comments