Skip to content

Instantly share code, notes, and snippets.

@holly
Last active March 1, 2026 16:30
Show Gist options
  • Select an option

  • Save holly/fdab4bade1955de90467ce72b586d02d to your computer and use it in GitHub Desktop.

Select an option

Save holly/fdab4bade1955de90467ce72b586d02d to your computer and use it in GitHub Desktop.
Automatic installation script for ripgrep, bat, fd, glow, duf, eza, shellcheck, lua-language-server (using standard modules only)
#!/usr/bin/env python3
"""
Automatic installation script for ripgrep, bat, fd, glow, duf, eza, shellcheck, lua-language-server (using standard modules only)
Fetches the latest versions from GitHub API, compares with existing versions, and updates them.
Also automatically installs fish shell completion files.
"""
import os
import sys
import json
import re
import urllib.request
import urllib.error
import subprocess
import tarfile
import shutil
import platform
import stat
from pathlib import Path
# Configuration
GITHUB_API_BASE_URL = "https://api.github.com/repos"
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") # Get GitHub token from environment variable
DOWNLOAD_DIR = Path.home() / "src"
INSTALL_DIR = Path.home() / ".local" / "bin"
TEMP_DIR_BASE = Path.home() / "tmp"
FISH_COMPLETIONS_DIR = Path.home() / ".config" / "fish" / "completions"
# Tool configurations
TOOLS = [
{
"name": "ripgrep",
"owner": "BurntSushi",
"binary_name": "rg",
"version_pattern": r"ripgrep (\d+\.\d+\.\d+)",
"completion_path": "complete/rg.fish",
"file_pattern": "ripgrep-{version}-{arch}-unknown-linux-musl.tar.gz",
"has_subdir": True
},
{
"name": "bat",
"owner": "sharkdp",
"binary_name": "bat",
"version_pattern": r"bat (\d+\.\d+\.\d+)",
"completion_path": "autocomplete/bat.fish",
"file_pattern": "bat-v{version}-{arch}-unknown-linux-musl.tar.gz",
"has_subdir": True
},
{
"name": "fd",
"owner": "sharkdp",
"binary_name": "fd",
"version_pattern": r"fd (\d+\.\d+\.\d+)",
"completion_path": "autocomplete/fd.fish",
"file_pattern": "fd-v{version}-{arch}-unknown-linux-musl.tar.gz",
"has_subdir": True
},
{
"name": "glow",
"owner": "charmbracelet",
"binary_name": "glow",
"version_pattern": r"glow version (\d+\.\d+\.\d+)",
"completion_path": "completions/glow.fish",
"file_pattern": "glow_{version}_Linux_{arch}.tar.gz",
"has_subdir": True
},
{
"name": "duf",
"owner": "muesli",
"binary_name": "duf",
"version_pattern": r"duf (\d+\.\d+\.\d+)",
"completion_path": "", # No fish completion file
"file_pattern": "duf_{version}_linux_{arch}.tar.gz",
"has_subdir": False # duf has no subdirectory
},
{
"name": "eza",
"owner": "eza-community",
"binary_name": "eza",
"version_pattern": r"v(\d+\.\d+\.\d+)", # Matches format like v0.23.4
"completion_path": "", # No completion file
"file_pattern": "eza_{arch}-unknown-linux-musl.tar.gz",
"has_subdir": False
},
{
"name": "shellcheck",
"owner": "koalaman",
"binary_name": "shellcheck",
"version_pattern": r"version: (\d+\.\d+\.\d+)", # Fixed: matches "version: 0.11.0"
"completion_path": "", # No fish completion file
"file_pattern": "shellcheck-v{version}.linux.{arch}.tar.gz",
"has_subdir": True,
"install_type": "extract_to_local" # Special install type: extract to ~/.local
},
{
"name": "lua-language-server",
"owner": "LuaLS",
"binary_name": "lua-language-server",
"version_pattern": r"(\d+\.\d+\.\d+)", # Fixed: matches "3.17.1"
"completion_path": "", # No fish completion file
"file_pattern": "lua-language-server-{version}-linux-{lua_arch}.tar.gz", # Fixed: use lua_arch
"has_subdir": True,
"install_type": "extract_to_custom_dir", # Special install type: extract to custom directory
"custom_dir": "lua-language-server",
"binary_path": "bin/lua-language-server" # Path to binary inside extracted directory
}
]
def get_system_architecture():
"""Get system architecture"""
machine = platform.machine().lower()
if machine in ("x86_64", "amd64"):
return {
"standard": "x86_64",
"lua": "x64"
}
elif machine in ("aarch64", "arm64"):
return {
"standard": "aarch64",
"lua": "arm64"
}
else:
print(f"Warning: Unsupported architecture {machine} detected. Using x86_64.")
return {
"standard": "x86_64",
"lua": "x64"
}
def main():
if len(sys.argv) != 2:
print("Usage: ./tools_install.py [install|uninstall]")
sys.exit(1)
action = sys.argv[1]
if action == "install":
print("Starting automatic installation of ripgrep, bat, fd, glow, duf, eza, shellcheck, lua-language-server...")
install_tools()
elif action == "uninstall":
print("Starting uninstallation of ripgrep, bat, fd, glow, duf, eza, shellcheck, lua-language-server...")
uninstall_tools()
else:
print("Invalid action. Use 'install' or 'uninstall'.")
sys.exit(1)
def install_tools():
# Check if GitHub token is set
if GITHUB_TOKEN:
print("GitHub token is set")
else:
print("Warning: GitHub token is not set. You may encounter rate limits.")
print("It's recommended to set a GitHub token in the GITHUB_TOKEN environment variable.")
print("You can create a token at https://github.com/settings/tokens")
# Get system architecture
arch_info = get_system_architecture()
print(f"System architecture: {arch_info['standard']}")
# Create directories
ensure_directories()
try:
# Install each tool
for tool in TOOLS:
print(f"\n--- Installing {tool['name']} ---")
install_tool(tool, arch_info)
print("\nInstallation completed")
except Exception as e:
print(f"An error occurred: {e}")
sys.exit(1)
def uninstall_tools():
try:
for tool in TOOLS:
print(f"\n--- Uninstalling {tool['name']} ---")
uninstall_tool(tool)
print("\nUninstallation completed")
except Exception as e:
print(f"An error occurred: {e}")
sys.exit(1)
def uninstall_tool(tool):
"""Uninstall the specified tool"""
binary_path = INSTALL_DIR / tool["binary_name"]
# Remove binary
if binary_path.exists():
binary_path.unlink()
print(f"Removed binary: {binary_path}")
# Remove fish completion file
if tool.get("completion_path", ""):
completion_file = FISH_COMPLETIONS_DIR / f"{tool['binary_name']}.fish"
if completion_file.exists():
completion_file.unlink()
print(f"Removed fish completion file: {completion_file}")
# Special handling for shellcheck
if tool["name"] == "shellcheck":
local_dir = Path.home() / ".local"
shellcheck_dir = local_dir / f"shellcheck-v{get_current_version(tool['binary_name'], tool['version_pattern'])}"
if shellcheck_dir.exists():
shutil.rmtree(shellcheck_dir)
print(f"Removed shellcheck directory: {shellcheck_dir}")
# Special handling for lua-language-server
if tool["name"] == "lua-language-server":
custom_dir = Path.home() / ".local" / tool["custom_dir"]
if custom_dir.exists():
shutil.rmtree(custom_dir)
print(f"Removed lua-language-server directory: {custom_dir}")
def ensure_directories():
"""Create necessary directories"""
DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
INSTALL_DIR.mkdir(parents=True, exist_ok=True)
FISH_COMPLETIONS_DIR.mkdir(parents=True, exist_ok=True)
# Create temporary directories for each tool
for tool in TOOLS:
temp_dir = TEMP_DIR_BASE / f"{tool['name']}_install"
temp_dir.mkdir(parents=True, exist_ok=True)
def install_tool(tool, arch_info):
"""Install the specified tool"""
# Get latest version info
api_url = f"{GITHUB_API_BASE_URL}/{tool['owner']}/{tool['name']}/releases/latest"
# Use appropriate architecture for the tool
if tool['name'] == 'lua-language-server':
arch = arch_info['lua']
else:
arch = arch_info['standard']
latest_version, download_url = get_latest_version_info(api_url, tool['name'], tool['file_pattern'], arch)
print(f"Latest {tool['name']} version: {latest_version}")
# Check current version
current_version = get_current_version(tool['binary_name'], tool['version_pattern'])
print(f"Current {tool['name']} version: {current_version}")
# Compare versions
if current_version and compare_versions(current_version, latest_version) >= 0:
print(f"Latest version of {tool['name']} ({latest_version}) is already installed")
return
# Download and install
download_and_install(tool, latest_version, download_url, arch)
print(f"Installation of {tool['name']} {latest_version} completed")
def get_latest_version_info(api_url, tool_name, file_pattern, arch):
"""Get latest version info and download URL from GitHub API"""
try:
# Set request headers
req = urllib.request.Request(api_url)
req.add_header('User-Agent', f'{tool_name}-installer/1.0')
req.add_header('Accept', 'application/vnd.github.v3+json')
# Add auth header if GitHub token is available
if GITHUB_TOKEN:
req.add_header('Authorization', f'token {GITHUB_TOKEN}')
# Make API request
with urllib.request.urlopen(req) as response:
data = json.loads(response.read().decode('utf-8'))
version = data['tag_name'].lstrip('v') # 'v15.1.0' -> '15.1.0'
# Construct file pattern based on architecture
expected_filename = file_pattern.format(version=version, arch=arch, lua_arch=arch)
# Get release file URL
download_url = None
for asset in data['assets']:
if asset['name'] == expected_filename:
download_url = asset['browser_download_url']
break
if not download_url:
print(f"Warning: {expected_filename} not found")
print("Available assets:")
for asset in data['assets']:
print(f" - {asset['name']}")
raise Exception(f"No suitable release file found: {expected_filename}")
return version, download_url
except urllib.error.HTTPError as e:
print(f"HTTP error occurred: {e.code} {e.reason}")
if e.code == 403:
print("You may have hit GitHub API rate limits")
if not GITHUB_TOKEN:
print("Setting a GitHub token in GITHUB_TOKEN environment variable will increase your rate limit")
print("You can create a token at https://github.com/settings/tokens")
sys.exit(1)
except urllib.error.URLError as e:
print(f"URL error occurred: {e.reason}")
sys.exit(1)
except json.JSONDecodeError:
print("Failed to parse JSON")
sys.exit(1)
except Exception as e:
print(f"Error during API access: {e}")
sys.exit(1)
def get_current_version(binary_name, version_pattern):
"""Get the version of the currently installed tool"""
try:
result = subprocess.run([binary_name, '--version'], capture_output=True, text=True, check=True)
# Combine stdout and stderr
output = result.stdout + result.stderr
# Process line by line to handle multiline output
lines = output.split('\n')
# Try version pattern on each line
for line in lines:
match = re.search(version_pattern, line)
if match:
return match.group(1)
# For debugging: show actual output
print(f"Debug: Output of {binary_name} --version:")
print(f"stdout: {result.stdout}")
print(f"stderr: {result.stderr}")
return None
except (subprocess.CalledProcessError, FileNotFoundError):
return None
def compare_versions(v1, v2):
"""Compare version strings (returns -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2)"""
def normalize(v):
return [int(x) for x in v.split('.')]
v1_parts = normalize(v1)
v2_parts = normalize(v2)
# Match part counts
max_len = max(len(v1_parts), len(v2_parts))
v1_parts.extend([0] * (max_len - len(v1_parts)))
v2_parts.extend([0] * (max_len - len(v2_parts)))
for i in range(max_len):
if v1_parts[i] < v2_parts[i]:
return -1
elif v1_parts[i] > v2_parts[i]:
return 1
return 0
def safe_extract(tar, path):
"""Safely extract tar file (security measures)"""
import pathlib
# Get absolute path of extraction destination
extract_path = pathlib.Path(path).resolve()
for member in tar.getmembers():
# Normalize member path
try:
member_path = pathlib.Path(member.name)
# Calculate absolute path of extraction destination
full_path = (extract_path / member_path).resolve()
# Verify extraction destination is within specified directory
if extract_path not in full_path.parents and full_path != extract_path:
print(f"Warning: Skipping member attempting directory traversal: {member.name}")
continue
except (ValueError, RuntimeError):
print(f"Warning: Skipping member with invalid path: {member.name}")
continue
# Be especially careful with symbolic links
if member.issym() or member.islnk():
print(f"Warning: Skipping symbolic link: {member.name}")
continue
# Keep original file mode for executable files
if member.isfile() and member.mode & stat.S_IXUSR:
# This is an executable file, keep its execute permissions
pass
elif member.isfile():
# Restrict file mode to safe values for non-executable files
member.mode = member.mode & 0o644
# Use filter parameter for safe extraction in Python 3.8+
try:
tar.extract(member, path=path, filter='data')
except AttributeError:
# For Python versions below 3.8, use traditional extraction
tar.extract(member, path=path)
def install_fish_completion(tool, version, temp_dir, arch):
"""Install fish shell completion file"""
# If no completion file is configured, do nothing
if not tool.get('completion_path', ''):
print(f"No fish completion file available for {tool['name']}")
return
# Guess extracted directory name
if tool['has_subdir']:
if tool['name'] == 'ripgrep':
extracted_dir = temp_dir / f"ripgrep-{version}-{arch}-unknown-linux-musl"
elif tool['name'] == 'bat':
extracted_dir = temp_dir / f"bat-v{version}-{arch}-unknown-linux-musl"
elif tool['name'] == 'fd':
extracted_dir = temp_dir / f"fd-v{version}-{arch}-unknown-linux-musl"
elif tool['name'] == 'glow':
extracted_dir = temp_dir / f"glow_{version}_Linux_{arch}"
src_file = extracted_dir / tool['completion_path']
else:
# If no subdirectory, it's directly in temp_dir
src_file = temp_dir / tool['completion_path']
dest_file = FISH_COMPLETIONS_DIR / f"{tool['binary_name']}.fish"
if src_file.exists():
print(f"Installing fish shell completion for {tool['name']}: {dest_file}")
shutil.copy2(str(src_file), str(dest_file))
print(f"Fish shell completion for {tool['name']} installed successfully")
else:
print(f"Completion file for {tool['name']} not found: {src_file}")
def download_and_install(tool, version, download_url, arch):
"""Download and install the tool"""
# Download
print(f"Downloading {tool['name']}: {download_url}")
# Get filename
filename = download_url.split('/')[-1]
download_path = DOWNLOAD_DIR / filename
# Temporary directory
temp_dir = TEMP_DIR_BASE / f"{tool['name']}_install"
try:
# Download
req = urllib.request.Request(download_url)
req.add_header('User-Agent', f"{tool['name']}-installer/1.0")
with urllib.request.urlopen(req) as response:
# Get file size
file_size = int(response.headers.get('Content-Length', 0))
# Show download progress
with open(download_path, 'wb') as f:
downloaded = 0
while True:
chunk = response.read(8192)
if not chunk:
break
f.write(chunk)
downloaded += len(chunk)
# Show progress
if file_size > 0:
percent = (downloaded / file_size) * 100
print(f"\rDownloading {tool['name']}: {percent:.1f}%", end='')
else:
print(f"\rDownloading {tool['name']}: {downloaded} bytes", end='')
print(f"\n{tool['name']} download completed")
# Extract (with security measures)
print(f"Extracting {tool['name']}...")
with tarfile.open(download_path, 'r:gz') as tar:
safe_extract(tar, temp_dir)
# Show extracted directory structure
print(f"{tool['name']} extracted directory structure:")
for root, dirs, files in os.walk(temp_dir):
level = root.replace(str(temp_dir), '').count(os.sep)
indent = ' ' * 2 * level
print(f"{indent}{os.path.basename(root)}/")
subindent = ' ' * 2 * (level + 1)
for file in files:
print(f"{subindent}{file}")
# Handle different installation types
install_type = tool.get('install_type', 'standard')
if install_type == 'extract_to_local':
# Special handling for shellcheck: extract to ~/.local
install_shellcheck(tool, version, temp_dir, arch)
elif install_type == 'extract_to_custom_dir':
# Special handling for lua-language-server: extract to custom directory
install_lua_language_server(tool, version, temp_dir, arch)
else:
# Standard installation
install_standard(tool, version, temp_dir, arch)
# Cleanup
shutil.rmtree(temp_dir)
download_path.unlink()
print(f"Successfully installed {tool['name']}")
except urllib.error.HTTPError as e:
print(f"\nHTTP error occurred while downloading {tool['name']}: {e.code} {e.reason}")
cleanup(download_path, temp_dir)
sys.exit(1)
except urllib.error.URLError as e:
print(f"\nURL error occurred while downloading {tool['name']}: {e.reason}")
cleanup(download_path, temp_dir)
sys.exit(1)
except Exception as e:
print(f"\nError occurred while downloading/installing {tool['name']}: {e}")
cleanup(download_path, temp_dir)
sys.exit(1)
def install_standard(tool, version, temp_dir, arch):
"""Standard installation method for most tools"""
# Identify binary file path
if tool['has_subdir']:
# If there's a subdirectory
if tool['name'] == 'ripgrep':
extracted_dir = temp_dir / f"ripgrep-{version}-{arch}-unknown-linux-musl"
elif tool['name'] == 'bat':
extracted_dir = temp_dir / f"bat-v{version}-{arch}-unknown-linux-musl"
elif tool['name'] == 'fd':
extracted_dir = temp_dir / f"fd-v{version}-{arch}-unknown-linux-musl"
elif tool['name'] == 'glow':
extracted_dir = temp_dir / f"glow_{version}_Linux_{arch}"
elif tool['name'] == 'eza':
extracted_dir = temp_dir / f"eza_v{version}_Linux_{arch}"
binary_path = extracted_dir / tool['binary_name']
else:
# If no subdirectory (like duf)
binary_path = temp_dir / tool['binary_name']
if not binary_path.exists():
print(f"Error: {tool['binary_name']} binary not found")
# Show all extracted files
print("Extracted files:")
for root, dirs, files in os.walk(temp_dir):
for file in files:
print(f" {Path(root) / file}")
sys.exit(1)
# Copy to installation directory
dest_file = INSTALL_DIR / tool['binary_name']
# Backup existing file
if dest_file.exists():
backup_file = INSTALL_DIR / f"{tool['binary_name']}.backup.{version}"
shutil.move(str(dest_file), str(backup_file))
print(f"Backed up existing {tool['name']} file to {backup_file}")
# Copy executable
shutil.copy2(str(binary_path), str(dest_file))
# Set execute permissions
os.chmod(str(dest_file), 0o755)
# Install fish shell completion
install_fish_completion(tool, version, temp_dir, arch)
print(f"Successfully installed {tool['name']} to {INSTALL_DIR}")
def install_shellcheck(tool, version, temp_dir, arch):
"""Special installation for shellcheck: extract to ~/.local"""
# shellcheck is extracted directly to ~/.local
local_dir = Path.home() / ".local"
# Find the extracted directory (should be shellcheck-v{version})
extracted_dir = None
for item in temp_dir.iterdir():
if item.is_dir() and item.name.startswith("shellcheck-v"):
extracted_dir = item
break
if not extracted_dir:
print(f"Error: Could not find shellcheck directory in {temp_dir}")
sys.exit(1)
# Remove existing installation if it exists
existing_install = local_dir / extracted_dir.name
if existing_install.exists():
print(f"Removing existing {tool['name']} installation: {existing_install}")
shutil.rmtree(existing_install)
# Move extracted directory to ~/.local
target_dir = local_dir / extracted_dir.name
shutil.move(str(extracted_dir), str(target_dir))
print(f"Moved {tool['name']} to {target_dir}")
# Find the shellcheck binary and set execute permissions
shellcheck_binary = target_dir / tool['binary_name']
if shellcheck_binary.exists():
os.chmod(str(shellcheck_binary), 0o755)
print(f"Set execute permissions on {shellcheck_binary}")
# Create symlink in ~/.local/bin
dest_file = INSTALL_DIR / tool['binary_name']
# Remove existing symlink if it exists
if dest_file.exists() or dest_file.is_symlink():
dest_file.unlink()
# Create new symlink
os.symlink(str(shellcheck_binary), str(dest_file))
print(f"Created symlink: {dest_file} -> {shellcheck_binary}")
def install_lua_language_server(tool, version, temp_dir, arch):
"""Special installation for lua-language-server: extract to custom directory"""
# Create custom directory if it doesn't exist
custom_dir = Path.home() / ".local" / tool['custom_dir']
custom_dir.mkdir(parents=True, exist_ok=True)
# Extract all contents to the custom directory
for item in temp_dir.iterdir():
if item.name != '.': # Skip hidden files if any
target = custom_dir / item.name
if target.exists():
if target.is_dir():
shutil.rmtree(target)
else:
target.unlink()
shutil.move(str(item), str(target))
print(f"Moved {tool['name']} to {custom_dir}")
# Find the lua-language-server binary and set execute permissions
binary_in_custom = custom_dir / tool['binary_path']
if binary_in_custom.exists():
os.chmod(str(binary_in_custom), 0o755)
print(f"Set execute permissions on {binary_in_custom}")
# Create symlink for the binary
dest_file = INSTALL_DIR / tool['binary_name']
# Remove existing symlink if it exists
if dest_file.exists() or dest_file.is_symlink():
dest_file.unlink()
# Create new symlink
os.symlink(str(binary_in_custom), str(dest_file))
print(f"Created symlink: {dest_file} -> {binary_in_custom}")
def cleanup(download_path, temp_dir):
"""Clean up temporary files"""
if download_path and download_path.exists():
download_path.unlink()
if temp_dir.exists():
shutil.rmtree(temp_dir)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment