Last active
May 2, 2024 16:19
-
-
Save BMTLab/54b62816de4c24983a1bc38418ce1e91 to your computer and use it in GitHub Desktop.
Script that manages symbolic links for other scripts to allow for the creation or deletion of symbolic links, making scripts executable from anywhere
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# Author: Nikita Neverov (BMTLab) | |
# Version: 1.1.3 | |
# Description: | |
# This script manages symbolic links for scripts within a specified directory. It allows | |
# for the creation or deletion of these symbolic links, making scripts executable from anywhere. | |
# This is particularly useful for managing utility scripts. | |
# Usage: | |
# chmod +x scripts_symlinks.sh | |
# sudo ./scripts_symlinks.sh [-d <target_dir>] [create|delete] <script1.sh script2.sh ... | * | all> | |
# - 'create': Creates symbolic links for specified or all scripts. | |
# - 'delete': Removes symbolic links for specified or all scripts. | |
# - '-d, --directory': Specifies a custom directory for symbolic links. Default is '/usr/local/bin'. | |
# - 'help': Displays this help message. | |
set -euo pipefail | |
set -E | |
# Constants | |
readonly SS_DEFAULT_TARGET_DIR='/usr/local/bin' | |
readonly SS_DEFAULT_ACTION='create' | |
readonly LOG_LEVEL_MIN='INFO' | |
readonly SS_ERR_REQUIRES_ROOT=2 | |
readonly SS_ERR_DIR_NOT_EXIST=5 | |
readonly SS_ERR_NO_WRITE_PERMISSIONS_TO_DIR=7 | |
readonly SS_ERR_INVALID_ACTION=10 | |
readonly SS_ERR_INVALID_ARGUMENT=13 | |
readonly SS_ERR_DIR_ALREADY_SET=14 | |
readonly SS_ERR_ACTION_ALREADY_SET=15 | |
readonly SS_ERR_NO_SCRIPT_SPECIFIED=20 | |
####################################### | |
# Displays usage information for the script. | |
# Outputs: | |
# Usage instructions to STDOUT. | |
####################################### | |
function ss_usage() { | |
cat <<EOF | |
Create or delete symbolic links to executable scripts | |
located in the current directory or current directory plus subdirectories into system folder, | |
allowing these scripts to be invoked from anywhere using only the script name. | |
Usage: | |
sudo $0 [-d <target_dir>] [$SS_DEFAULT_ACTION] <s1.sh s2.sh ...| * | self | all> | delete <script1.sh script2.sh ... | * | self | all> | |
create : (Optional) Creates symbolic links for the specified scripts or all executable scripts (*.sh with +x) | |
if "all" is used, all scripts in the current directory and subdirectories will be added; | |
if "*" is used, only scripts in the current directory will be added. | |
if "self" is used, this script itself will be added. | |
It is also possible to combine patterns, like '.. create all self ../script-1-1.sh script-2-*'. | |
[Requires root privileges] | |
[Default action] | |
delete : Removes symbolic links for the specified scripts or all scripts (*.sh), | |
if "all" is used, all scripts in the current directory and subdirectory will be removed; | |
if * is used, only scripts in the current directory will be removed. | |
if "self" is used, this script itself will be removed. | |
It is also possible to combine patterns, like '.. delete all self ../script-1-1.sh script-2-*'. | |
[Requires root privileges] | |
-d, --directory : Target directory for symbolic links (default: $SS_DEFAULT_TARGET_DIR). | |
help, -h, --help : Display this help message. | |
EOF | |
} | |
####################################### | |
# Logs messages to STDOUT or STDERR depending on the severity. | |
# Arguments: | |
# 1 - Log level ('INFO' or 'ERROR'). | |
# 2 - Message to log. | |
# 3 - Exit error code (optional). | |
# Outputs: | |
# Error message to STDERR if log level is 'ERROR'. | |
# Information message to STDOUT if log level is 'INFO'. | |
####################################### | |
function __ss_log() { | |
local level="$1" | |
local message="$2" | |
local -ir exit_code=${3:-1} | |
# Determine if the message should be logged based on LOG_LEVEL_MIN | |
if [[ $LOG_LEVEL_MIN == 'INFO' ]] || [[ $level == 'ERROR' ]]; then | |
if [[ $level == 'ERROR' ]]; then | |
# Output to stderr for ERROR | |
printf '[%s] %s. Code: %d.\n' "$level" "$message" "$exit_code" >&2 | |
else | |
# Output to stdout for INFO | |
printf '[%s] %s.\n' "$level" "$message" | |
fi | |
fi | |
} | |
####################################### | |
# Logs an error message and exits with a provided error code. | |
# Arguments: | |
# 1 - Error message. | |
# 2 - Error code (defaults to 1). | |
# Outputs: | |
# Writes the error message to STDERR. | |
# Returns: | |
# Always returns with the error code. | |
####################################### | |
function __ss_error() { | |
# Use 1 as the default if no error code is specified. | |
local -ir exit_code=${2:-1} | |
local -r error_message="$1" | |
__ss_log 'ERROR' "$error_message" $exit_code || true | |
return $exit_code | |
} | |
####################################### | |
# Ensures the script is run with root privileges. | |
# Exits if not run as root. | |
# Outputs: | |
# Error message if not run as root. | |
# Returns: | |
# Returns an error code on failure. | |
####################################### | |
function __ss_ensure_root() { | |
if [[ $EUID -ne 0 ]]; then | |
__ss_error 'This script must be run as root' \ | |
$SS_ERR_REQUIRES_ROOT || | |
return $? | |
fi | |
} | |
####################################### | |
# Checks if the target directory exists and if it has write permissions. | |
# Arguments: | |
# 1 - Directory to check. | |
# Outputs: | |
# Error messages if checks fail. | |
# Returns: | |
# Returns an error code on failure. | |
####################################### | |
function __ss_ensure_dir() { | |
local -r dir_to_check="$1" | |
if [[ ! -d $dir_to_check ]]; then | |
__ss_error "Directory does not exist: '$dir_to_check'" \ | |
$SS_ERR_DIR_NOT_EXIST || | |
return $? | |
elif [[ ! -w $dir_to_check ]]; then | |
__ss_error "No write permissions for the target directory: '$dir_to_check'" \ | |
$SS_ERR_NO_WRITE_PERMISSIONS_TO_DIR || | |
return $? | |
fi | |
} | |
####################################### | |
# Parses command-line arguments and sets global variables accordingly. | |
# Arguments: | |
# Command-line arguments. | |
# Outputs: | |
# Error messages for invalid arguments. | |
# Returns: | |
# Returns an error code on failure. | |
####################################### | |
function __ss_parse_args() { | |
local -n _action=$1 | |
local -n _target_dir=$2 | |
# For whatever reason, we can't pass an array by reference when a variable is explicitly defined as an array (-a option). | |
# However, if we do not specify that it is an array, everything works. | |
# It is possible that something here may break over time. | |
local -n _scripts=$3 | |
local is_dir_set=false | |
local is_action_set=false | |
while [[ $# -gt 0 ]]; do | |
case $1 in | |
-d | --directory) | |
if [[ $is_dir_set == false ]]; then | |
_target_dir="$2" | |
__ss_ensure_dir "$_target_dir" || return $? | |
is_dir_set=true | |
shift 2 | |
else | |
__ss_error 'Directory is already set' \ | |
$SS_ERR_DIR_ALREADY_SET || | |
return $? | |
fi | |
;; | |
create | delete) | |
if [[ $is_action_set == false ]]; then | |
_action="$1" | |
__ss_ensure_root || return $? | |
is_action_set=true | |
shift | |
else | |
__ss_error 'Action is already set' \ | |
$SS_ERR_ACTION_ALREADY_SET || | |
return $? | |
fi | |
;; | |
help | -h | --help) | |
ss_usage | |
return 1 | |
;; | |
*) | |
if [[ $_action ]]; then | |
local arg="$1" | |
local script_name | |
if [[ $arg == 'self' ]]; then | |
_scripts+=("$(realpath "$0")") | |
elif [[ $arg == 'all' ]]; then | |
local -a found_scripts | |
local -ar filter=(find . -maxdepth 10 -executable -name '*.sh' ! -name '_*' ! -name "$(basename "$0").sh" ! -path '*/\.*' ! -type l) | |
mapfile -t found_scripts < <("${filter[@]}") | |
for script_name in "${found_scripts[@]}"; do | |
_scripts+=("$(realpath "$script_name")") | |
done | |
elif [[ -f $arg && ${arg##*.} == 'sh' ]]; then | |
script_name="$arg" | |
# Check that the file is executable, is not a symbolic link, is not a startup script, and the name does not start with '_' | |
if [[ -f $script_name && -x $script_name && ! -L $script_name && "$(realpath "$script_name")" != "$(realpath "$0")" && ${script_name:0:1} != '_' ]]; then | |
_scripts+=("$(realpath "$script_name")") | |
elif [[ "$(realpath "$script_name")" != "$(realpath "$0")" ]]; then | |
__ss_log 'TRACE' "Ignoring '$script_name' due to invalid properties (non-executable, a link, or the script itself)" | |
fi | |
fi | |
else | |
__ss_error "Invalid argument: $arg" \ | |
$SS_ERR_INVALID_ARGUMENT || | |
return $? | |
fi | |
shift | |
;; | |
esac | |
done | |
# Last check that script names or patterns have been specified | |
if [[ ${#_scripts[@]} -eq 0 ]]; then | |
__ss_error 'No scripts specified' \ | |
$SS_ERR_NO_SCRIPT_SPECIFIED || | |
return $? | |
fi | |
} | |
####################################### | |
# Manages (creates or deletes) symbolic links based on the specified action. | |
# Arguments: | |
# 1 - Action to perform ('create' or 'delete'). | |
# 2 - Target directory for symbolic links. | |
# @ - Scripts for which to manage symbolic links. | |
# Outputs: | |
# Log messages about the actions taken. | |
# Returns: | |
# None | |
####################################### | |
function __ss_manage_symlinks() { | |
local action="$1" | |
local target_dir="$2" | |
shift 2 | |
local script | |
local abs_script_location | |
local target_link | |
local target_link_no_ext | |
for script in "$@"; do | |
abs_script_location=$(realpath "$script") | |
target_link="${target_dir}/$(basename "$script")" | |
target_link_no_ext="${target_dir}/$(basename -s .sh "$script")" | |
case $action in | |
create) | |
local -i created_links=0 | |
if ! [[ -L $target_link ]]; then | |
ln -s "$abs_script_location" "$target_link" | |
created_links=1 | |
fi | |
if ! [[ -L $target_link_no_ext ]]; then | |
ln -s "$abs_script_location" "$target_link_no_ext" | |
created_links=$((created_links + 2)) | |
fi | |
case $created_links in | |
0) | |
__ss_log 'WARN' "Symlinks already exist for '$script'" | |
;; | |
1) | |
__ss_log 'INFO' "Created symlink for '$script' (with extension)" | |
;; | |
2) | |
__ss_log 'INFO' "Created symlink for script '$script' (no extension)" | |
;; | |
3) | |
__ss_log 'INFO' "Created symlinks for '$script' (with and without extension)" | |
;; | |
esac | |
continue | |
;; | |
delete) | |
local -i deleted_links=0 | |
if [[ -L $target_link ]]; then | |
rm "$target_link" | |
deleted_links=1 | |
fi | |
if [[ -L $target_link_no_ext ]]; then | |
rm "$target_link_no_ext" | |
deleted_links=$((deleted_links + 2)) | |
fi | |
case $deleted_links in | |
0) | |
__ss_log 'WARN' "Symlinks not found for '$script'" | |
;; | |
1) | |
__ss_log 'INFO' "Deleted symlink for '$script' (with extension)" | |
;; | |
2) | |
__ss_log 'INFO' "Deleted symlink for '$script' (no extension)" | |
;; | |
3) | |
__ss_log 'INFO' "Deleted symlinks for '$script' (with and without extension)" | |
;; | |
esac | |
continue | |
;; | |
*) | |
__ss_error "Invalid action: '$action'. Only 'create' and 'delete' are allowed" \ | |
$SS_ERR_INVALID_ACTION || | |
return $? | |
;; | |
esac | |
done | |
} | |
####################################### | |
# The main function that orchestrates the script's operations. | |
# Globals: | |
# Uses constants 'SS_DEFAULT_ACTION' and 'SS_DEFAULT_TARGET_DIR'. | |
# Arguments: | |
# Command-line arguments. | |
# Returns: | |
# Script's exit code. | |
####################################### | |
function main() { | |
local action="$SS_DEFAULT_ACTION" | |
local target_dir="$SS_DEFAULT_TARGET_DIR" | |
local -a scripts=() | |
__ss_parse_args action target_dir scripts "$@" && | |
__ss_manage_symlinks "$action" "$target_dir" "${scripts[@]}" | |
} | |
# Execute the main function with all passed arguments | |
main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment