@@ -551,8 +551,10 @@ def add_directory_to_windows_path(directory: str) -> tuple[bool, str]:
551551 winreg .CloseKey (reg_key )
552552 return (
553553 False ,
554- f'PATH too long ({ len (new_path )} chars, limit 1024). '
555- f'Please manually add: { normalized_dir } ' ,
554+ (
555+ f'PATH too long ({ len (new_path )} chars, limit 1024). '
556+ f'Please manually add: { normalized_dir } '
557+ ),
556558 )
557559
558560 # Write new PATH to registry
@@ -1841,6 +1843,8 @@ def get_all_shell_config_files() -> list[Path]:
18411843 home / '.zshenv' , # All zsh instances (recommended for env vars)
18421844 home / '.zprofile' , # Zsh login shells (macOS default since Catalina)
18431845 home / '.zshrc' , # Interactive zsh shells
1846+ # Fish files
1847+ home / '.config' / 'fish' / 'config.fish' , # Fish shell config
18441848 ]
18451849
18461850 # On Linux, only include zsh files if zsh is installed
@@ -1850,14 +1854,55 @@ def get_all_shell_config_files() -> list[Path]:
18501854 # Filter out zsh-specific files if zsh is not installed
18511855 config_files = [f for f in config_files if not f .name .startswith ('.zsh' )]
18521856
1857+ # Only include fish config if fish is installed
1858+ fish_path = shutil .which ('fish' )
1859+ if not fish_path :
1860+ config_files = [f for f in config_files if 'fish' not in str (f )]
1861+
18531862 return config_files
18541863
18551864
1865+ def _get_export_line (config_file : Path , name : str , value : str ) -> str :
1866+ """Generate the appropriate export line for the shell type.
1867+
1868+ Args:
1869+ config_file: Path to the shell config file.
1870+ name: Environment variable name.
1871+ value: Environment variable value.
1872+
1873+ Returns:
1874+ str: The export line in the appropriate syntax for the shell.
1875+ """
1876+ # Fish shell uses different syntax
1877+ if 'fish' in str (config_file ):
1878+ return f'set -gx { name } "{ value } "'
1879+ # Bash/Zsh use export
1880+ return f'export { name } ="{ value } "'
1881+
1882+
1883+ def _get_export_prefix (config_file : Path , name : str ) -> str :
1884+ """Get the line prefix to match for an existing export.
1885+
1886+ Args:
1887+ config_file: Path to the shell config file.
1888+ name: Environment variable name.
1889+
1890+ Returns:
1891+ str: The prefix to match (e.g., 'export NAME=' or 'set -gx NAME ').
1892+ """
1893+ # Fish shell uses different syntax
1894+ if 'fish' in str (config_file ):
1895+ return f'set -gx { name } '
1896+ # Bash/Zsh use export
1897+ return f'export { name } ='
1898+
1899+
18561900def add_export_to_file (config_file : Path , name : str , value : str ) -> bool :
18571901 """Add or update an environment variable export in a shell config file.
18581902
18591903 Uses markers to manage a block of exports set by claude-code-toolbox.
18601904 Updates existing variables within the block, or adds new ones.
1905+ Automatically uses the correct syntax for the shell type (bash/zsh vs fish).
18611906
18621907 Args:
18631908 config_file: Path to the shell config file.
@@ -1867,7 +1912,8 @@ def add_export_to_file(config_file: Path, name: str, value: str) -> bool:
18671912 Returns:
18681913 bool: True if successful, False otherwise.
18691914 """
1870- export_line = f'export { name } ="{ value } "'
1915+ export_line = _get_export_line (config_file , name , value )
1916+ export_prefix = _get_export_prefix (config_file , name )
18711917
18721918 try :
18731919 # Read existing content
@@ -1902,7 +1948,7 @@ def add_export_to_file(config_file: Path, name: str, value: str) -> bool:
19021948 for line in block_lines :
19031949 if line in (ENV_VAR_MARKER_START , ENV_VAR_MARKER_END ):
19041950 continue
1905- if line .strip ().startswith (f'export { name } =' ):
1951+ if line .strip ().startswith (export_prefix ):
19061952 # Update existing variable
19071953 new_block_lines .append (export_line )
19081954 found = True
@@ -1941,11 +1987,99 @@ def add_export_to_file(config_file: Path, name: str, value: str) -> bool:
19411987 return False
19421988
19431989
1990+ def _is_bash_zsh_export_line (line : str , name : str ) -> bool :
1991+ """Check if a line is a bash/zsh export for the given variable name.
1992+
1993+ Matches patterns:
1994+ - export NAME="value"
1995+ - export NAME='value'
1996+ - export NAME=value
1997+ - NAME="value" (without export keyword)
1998+
1999+ Does NOT match:
2000+ - Comments containing the variable name
2001+ - Lines where the variable name is part of another word
2002+
2003+ Args:
2004+ line: The line to check.
2005+ name: The environment variable name.
2006+
2007+ Returns:
2008+ bool: True if line exports the variable, False otherwise.
2009+ """
2010+ stripped = line .strip ()
2011+
2012+ # Skip comments
2013+ if stripped .startswith ('#' ):
2014+ return False
2015+
2016+ # Match "export NAME=" or "NAME=" patterns
2017+ # Must be at start of line (after stripping) to avoid partial matches
2018+ return stripped .startswith ((f'export { name } =' , f'{ name } =' ))
2019+
2020+
2021+ def _is_fish_set_line (line : str , name : str ) -> bool :
2022+ """Check if a line is a fish shell set command for the given variable name.
2023+
2024+ Matches patterns:
2025+ - set -gx NAME "value"
2026+ - set -gx NAME 'value'
2027+ - set -Ux NAME "value"
2028+ - set NAME "value"
2029+ - And variations with different flag orders
2030+
2031+ Does NOT match:
2032+ - Comments containing the variable name
2033+ - Lines where the variable name is part of another word
2034+
2035+ Args:
2036+ line: The line to check.
2037+ name: The environment variable name.
2038+
2039+ Returns:
2040+ bool: True if line sets the variable, False otherwise.
2041+ """
2042+ stripped = line .strip ()
2043+
2044+ # Skip comments
2045+ if stripped .startswith ('#' ):
2046+ return False
2047+
2048+ # Fish shell set pattern: set [-flags] NAME value
2049+ # Pattern matches: set (with optional flags like -gx, -Ux, etc.) followed by NAME and value
2050+ fish_pattern = rf'^set\s+(?:-[gGxXUu]+\s+)*{ re .escape (name )} \s+'
2051+ return bool (re .match (fish_pattern , stripped ))
2052+
2053+
2054+ def _is_env_var_line (config_file : Path , line : str , name : str ) -> bool :
2055+ """Check if a line sets the given environment variable (any shell syntax).
2056+
2057+ Detects the shell type from the config file path and checks accordingly.
2058+
2059+ Args:
2060+ config_file: Path to the shell config file.
2061+ line: The line to check.
2062+ name: The environment variable name.
2063+
2064+ Returns:
2065+ bool: True if line sets the variable, False otherwise.
2066+ """
2067+ # Check if this is a fish config file
2068+ is_fish = 'fish' in str (config_file )
2069+
2070+ if is_fish :
2071+ return _is_fish_set_line (line , name )
2072+ return _is_bash_zsh_export_line (line , name )
2073+
2074+
19442075def remove_export_from_file (config_file : Path , name : str ) -> bool :
19452076 """Remove an environment variable export from a shell config file.
19462077
1947- Removes the variable from the claude-code-toolbox marker block.
1948- If the block becomes empty, removes the entire block.
2078+ Removes the variable from:
2079+ 1. The claude-code-toolbox marker block (if exists)
2080+ 2. ALSO from anywhere else in the file (legacy/manual additions)
2081+
2082+ If the marker block becomes empty after removal, removes the entire block.
19492083
19502084 Args:
19512085 config_file: Path to the shell config file.
@@ -1959,45 +2093,49 @@ def remove_export_from_file(config_file: Path, name: str) -> bool:
19592093
19602094 try :
19612095 content = config_file .read_text (encoding = 'utf-8' )
2096+ original_content = content
2097+ lines = content .split ('\n ' )
2098+ new_lines : list [str ] = []
19622099
1963- if ENV_VAR_MARKER_START not in content :
1964- # No marker block, nothing to remove
1965- return True
1966-
1967- start_idx = content .find (ENV_VAR_MARKER_START )
1968- end_idx = content .find (ENV_VAR_MARKER_END )
1969-
1970- if end_idx == - 1 :
1971- end_idx = len (content )
1972-
1973- before = content [:start_idx ]
1974- block = content [start_idx : end_idx + len (ENV_VAR_MARKER_END )]
1975- after = content [end_idx + len (ENV_VAR_MARKER_END ) :]
1976-
1977- # Parse and filter the block
1978- block_lines = block .split ('\n ' )
1979- new_block_lines : list [str ] = []
1980-
1981- for line in block_lines :
1982- if line in (ENV_VAR_MARKER_START , ENV_VAR_MARKER_END ):
1983- continue
1984- if line .strip ().startswith (f'export { name } =' ):
2100+ # First pass: Remove the variable from ANYWHERE in the file
2101+ for line in lines :
2102+ if _is_env_var_line (config_file , line , name ):
19852103 # Skip this line (remove the variable)
19862104 continue
1987- if line .strip ():
1988- new_block_lines .append (line )
1989-
1990- # Reconstruct
1991- if new_block_lines :
1992- # Block still has content
1993- new_block = ENV_VAR_MARKER_START + '\n ' + '\n ' .join (new_block_lines ) + '\n ' + ENV_VAR_MARKER_END
1994- new_content = before + new_block + after
1995- else :
1996- # Block is empty, remove it entirely
1997- # Clean up extra newlines
1998- new_content = before .rstrip ('\n ' ) + '\n ' + after .lstrip ('\n ' )
1999-
2000- config_file .write_text (new_content , encoding = 'utf-8' )
2105+ new_lines .append (line )
2106+
2107+ new_content = '\n ' .join (new_lines )
2108+
2109+ # Clean up empty marker blocks
2110+ if ENV_VAR_MARKER_START in new_content :
2111+ start_idx = new_content .find (ENV_VAR_MARKER_START )
2112+ end_idx = new_content .find (ENV_VAR_MARKER_END )
2113+
2114+ if end_idx != - 1 :
2115+ before = new_content [:start_idx ]
2116+ block = new_content [start_idx : end_idx + len (ENV_VAR_MARKER_END )]
2117+ after = new_content [end_idx + len (ENV_VAR_MARKER_END ) :]
2118+
2119+ # Check if block is empty (only markers and whitespace)
2120+ block_lines = block .split ('\n ' )
2121+ has_content = False
2122+ for block_line in block_lines :
2123+ if block_line in (ENV_VAR_MARKER_START , ENV_VAR_MARKER_END ):
2124+ continue
2125+ if block_line .strip ():
2126+ has_content = True
2127+ break
2128+
2129+ if not has_content :
2130+ # Block is empty, remove it entirely
2131+ new_content = before .rstrip ('\n ' ) + '\n ' + after .lstrip ('\n ' )
2132+ # Handle edge case where file becomes only newlines
2133+ if new_content .strip () == '' :
2134+ new_content = ''
2135+
2136+ # Only write if content changed
2137+ if new_content != original_content :
2138+ config_file .write_text (new_content , encoding = 'utf-8' )
20012139 return True
20022140
20032141 except OSError as e :
0 commit comments