Skip to content

Instantly share code, notes, and snippets.

@tgran2028
Created June 28, 2024 01:30
Show Gist options
  • Save tgran2028/904679aff55355702a9a7f7112cf8c5a to your computer and use it in GitHub Desktop.
Save tgran2028/904679aff55355702a9a7f7112cf8c5a to your computer and use it in GitHub Desktop.
Shell command line tool for managing path
#!/usr/bin/env python3
import enum
import os
import sys
from pathlib import Path
from typing import List, Literal, Optional
import click
import rich
import rich.table
import typer
app = typer.Typer(
name="path-util",
add_completion=True,
add_help_option=True,
help="Utility to manage PATH environment variable.",
no_args_is_help=True,
)
def get_path() -> List[str]:
"""Get PATH environment variable.
Returns:
List[str]: PATH environment variable split into a list of directories.
"""
return os.environ["PATH"].split(os.pathsep)
def deduplicate_path(path: Optional[List[str] | str] = None) -> List[str]:
"""Deduplicate PATH environment variable while preserving order.
Args:
path (Optional[List[str] | str], optional): List of directories or PATH environment variable. Defaults to None.
Returns:
List[str]: Deduplicated PATH environment variable.
"""
if path is None:
path = get_path()
if isinstance(path, str):
path = path.split(os.pathsep)
new_path = []
for p in path:
if p not in new_path:
new_path.append(p)
return new_path
def abs_path(path: str | Path) -> Path:
"""Get absolute path with user expansion.
Args:
path (str | Path): Path to file or directory.
Returns:
Path: Absolute path.
"""
if isinstance(path, str):
path = Path(path)
return path.expanduser().absolute()
def _remove_from_path(path: str | Path,
path_list: Optional[List[str]] = None) -> List[str]:
"""Remove a path from PATH.
Args:
path (str | Path): Path to remove.
path_list (Optional[List[str]], optional): Path list. If None, get PATH environment variable. Defaults to None.
Returns:
List[str]: List of items in PATH with path removed.
"""
if path_list is None:
path_list = get_path()
if isinstance(path, Path):
path = str(path)
new_path = []
for p in path_list:
if p != path:
new_path.append(p)
return new_path
def _add_to_path(
path: str | Path,
position: Literal["append", "prepend"] = "append") -> List[str]:
path = abs_path(path)
path_list = get_path()
path_list = deduplicate_path(path_list)
if path in path_list:
path_list = remove_from_path(path, path_list)
if position == "prepend":
path_list.insert(0, str(path))
else:
path_list.append(str(path))
return path_list
def filepath_completer(ctx: click.Context, args: List[str], incomplete: str):
return [
str(p) for p in Path(".").iterdir()
if p.is_dir() and p.name.startswith(incomplete)
]
class Position(enum.Enum):
APPEND = "append"
PREPEND = "prepend"
@app.command(name="add")
def add_to_path(
path: str = typer.Argument(
...,
help="Path to add to PATH",
exists=True,
metavar="<DIRECTORY>",
show_default=False,
autocompletion=filepath_completer,
),
position: Position = typer.Option(
"append",
"-p",
"--position",
help="Position to add path to PATH",
show_default=True,
case_sensitive=False,
show_choices=True,
),
deduplicate: bool = typer.Option(
False,
"-d",
"--deduplicate",
help="Deduplicate PATH",
show_default=True,
show_choices=False,
is_flag=True,
),
):
"""
Add a path to PATH.
"""
pos = Position(position)
path = abs_path(path)
path_list = _add_to_path(path, position=str(pos))
if pos == Position.PREPEND:
path_list.insert(0, str(path))
else:
path_list.append(str(path))
if deduplicate:
path_list = deduplicate_path(path_list)
typer.echo(os.pathsep.join(path_list), nl=False)
# @app.command(name="search")
# def search_path(
# text: str = typer.Argument(
# ...,
# help="Text to search in PATH",
# metavar="<TEXT>",
# show_default=False,
# autocompletion=None,
# show_choices=False,
# )
# ):
# """
# Search for a text in PATH.
# """
# path_list = get_path()
# if text.startswith("~"):
# text = abs_path(text)
# path_str = os.pathsep.join(path_list)
# path_str = typer.style(path_str, fg=typer.colors.WHITE, dim=True)
# path_str.replace(
# text, typer.style(text, fg=typer.colors.MAGENTA, bold=True, dim=False)
# )
# typer.echo(path_str, nl=False, color=True)
@app.command(name="remove")
def remove_from_path(path: str = typer.Argument(
...,
help="Path to add remove from PATH",
metavar="<DIRECTORY>",
show_default=False,
autocompletion=filepath_completer,
)):
"""
Remove a path from PATH.
"""
path = abs_path(path)
path_list = _remove_from_path(path)
path_list = [p for p in path_list if not Path(path).samefile(p)]
typer.echo(os.pathsep.join(path_list), nl=False)
@app.command(name="contains")
def is_in_path(path: str = typer.Argument(
...,
help="Path to check if in PATH",
metavar="<DIRECTORY>",
show_default=False,
autocompletion=filepath_completer,
)):
"""
Check if a path is in PATH.
"""
path = abs_path(path)
path_list = get_path()
if str(path) in path_list:
typer.Exit(0)
else:
typer.Exit(1)
@app.command(name="deduplicate")
def deduplicate_path_cmd():
"""
Deduplicate PATH.
"""
path_list = deduplicate_path()
typer.echo(os.pathsep.join(path_list), nl=False)
@app.command(name="show")
def show_path():
"""
Show PATH.
"""
path_list = get_path()
tbl = rich.table.Table(
show_header=True,
header_style="bold",
title="PATH",
title_style="bold",
show_lines=True,
show_edge=False,
)
tbl.add_column("Path", justify="left")
for p in path_list:
tbl.add_row(p)
console = rich.console.Console()
console.print(tbl)
if __name__ == "__main__":
app()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment