diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..00261e3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,36 @@ +# EditorConfig is awesome: https://editorconfig.org + +root = true +# top-most EditorConfig file +# root: special property that should be specified at the top of the file outside of any sections. +# Set to "true" to stop .editorconfig files search on current file. + +[*] +# Defaults + +indent_style= tab +# set to "tab" or "space" to use hard tabs or soft tabs respectively. + +indent_size = 2 +# a whole number defining the number of columns used for each indentation level and the width of soft tabs (when supported). +# When set to "tab", the value of tab_width (if specified) will be used. + +# tab_width = 2 +# a whole number defining the number of columns used to represent a tab character. +# This defaults to the value of indent_size and doesn't usually need to be specified. + +end_of_line = lf +# set to "lf", "cr", or "crlf" to control how line breaks are represented. + +charset = utf-8 +# set to "latin1", "utf-8", "utf-8-bom", "utf-16be" or "utf-16le" to control the character set. + +insert_final_newline = true +# set to "true" to ensure file ends with a newline when saving and "false" to ensure it doesn't. + +trim_trailing_whitespace = true +# set to "true" to remove any whitespace characters preceding newline characters and "false" to ensure it doesn't. + +max_line_length = 80 +# Forces hard line wrapping after the amount of characters specified. +# "unset" to turn off this feature (use the editor settings). diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..39cdd10 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# Use LF on checkout across all platforms (Linux, MacOSX, Windows) +# as most text files work well with LF on Windows. +# Exceptions can be added below. +* text eol=lf + +# Files that should have their EOL on checkout changed to CRLF. + +# Files that should retain their EOL on checkout but may be diff'ed. + +# Files that are truly binary - no EOL change on checkout and no diff. +*.png binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..566bdf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# vim +*.swp diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8e9fea5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,113 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog] +and this project adheres to [Semantic Versioning]. + +## [Unreleased] +### Added +### Changed +* 2026-04-21 8fa6c8b 💥 __BREAKING__ install: use [UAPI](https://uapi-group.org/specifications/specs/linux_file_system_hierarchy/#locallib) +LIB directory [go2null] + standard. +### Deprecated +### Removed +### Fixed +* 2026-04-21 a32cb26 🐛 fix: ensure ssh agent is loaded before using ssh [go2null] +* 2026-01-03 a53c3a9 🐛 fix logic bug - exit with success status if already sourced [go2null] +### Security +* 2026-04-21 e643931 🥅 add error trapping to sshag_ssh_* [go2null] +* 2026-01-03 fa4dc98 🥅 use explicit return codes [go2null] + +## [3.0.3] - 2025-04-23 +### Added +* _go2null_: 🔨 devex: set git EOL and EditorConfig defaults + +## [3.0.2] - 2025-04-04 +### Changed +* _go2null_: 💄 removed extra whitespace from keys list +* _go2null_: ⬆️ pearl: update to new standard + +## [3.0.1] - 2025-04-03 +### Fixed +* _go2null_: ✏️ removed example code + +## [3.0.0] - 2025-04-03 +### Added +* _go2null_: ✨ runs ssh-add automatically when shell starts +* _go2null_: ✨ migrate existing installs to the new location +### Changed +* _go2null_: __BREAKING__: 💥 install to XDG_DATA_DIR/lib Directory +### Fixed +* _go2null_: 🐛 fix detecting when sourced in ZSH + +## [2.0.0] - 2022-04-01 +### Fixed +* _go2null_: Fix bug #2 path to LICENSE. +### Added +* _go2null_: Ability to uninstall. +### Changed +* _go2null_: __BREAKING__: install now defaults to _system_ only if running as `root`. +* _go2null_: __BREAKING__: install now defaults to `~/.local/lib` per `systemd` standard. + +## [1.3.1] - 2018-02-19 +### Added +* _go2null_: Added support for [pearl] shell package manager. +### Changed +* _go2null_: Replaced regular `git` tags with annotated tags with changelog. +* _go2null_: Moved __History__ section from `README.markdown` to `CHANGELOG.md`. +* _go2null_: Renamed `README.markdown` to `README.md`. + +## [1.3.0] - 2018-01-17 +### Added +* _go2null_: Allow passing arguments/options to `ssh`. +* _go2null_: New `install` and `update` functions. + +## [1.2.1] - 2017-10-07 +## Added +* _go2null_: Check if `ssh` supports `AddKeysToAgent` flag. +## Changed +* _go2null_: Fixed detection of identity files. +* _go2null_: Fixed grep error when config file not found. + +## [1.2.0] - 2016-08-25 +### Added +* _go2null_: Search `$TMPDIR` for agents as well, per OpenSSH man page. +* _go2null_: Accept socket passed in. +* _go2null_: Can now use `sshag user@domain` instead of `ssh user@domain`. +### Changed +* _go2null_: Make script POSIX compliant. + +## [1.1.0] - 2011-02-20 +### Added +* _intuited_: Made it convenient to run the script in a subshell. + +## [1.0.0] - 2010-07-26 +### Added +* _intuited_: Add readme and license documents. +### Changed +* _intuited_: __BREAKING__: Renamed from `sagent` to `sshag`. + +## [0.0.0] - 2010-05-14 +### Added +* _Zed_: http://superuser.com/a/141241 + + +[Keep a Changelog]: http://keepachangelog.com +[Semantic Versioning]: http://semver.org +[pearl]: https://github.com/pearl-core/pearl#installation + +[Unreleased]: https://github.com/go2null/sshag/compare/3.0.3...HEAD +[3.0.3]: https://github.com/go2null/sshag/compare/3.0.2...3.0.3 +[3.0.2]: https://github.com/go2null/sshag/compare/3.0.1...3.0.2 +[3.0.1]: https://github.com/go2null/sshag/compare/3.0.0...3.0.1 +[3.0.0]: https://github.com/go2null/sshag/compare/2.0.0...3.0.0 +[2.0.0]: https://github.com/go2null/sshag/compare/1.3.0...2.0.0 +[1.3.1]: https://github.com/go2null/sshag/compare/1.3.0...1.3.1 +[1.3.0]: https://github.com/go2null/sshag/compare/1.2.1...1.3.0 +[1.2.1]: https://github.com/go2null/sshag/compare/1.2.0...1.2.1 +[1.2.0]: https://github.com/go2null/sshag/compare/1.1.0...1.2.0 +[1.1.0]: https://github.com/go2null/sshag/compare/1.0.0...1.1.0 +[1.0.0]: https://github.com/go2null/sshag/compare/0.0.0...1.0.0 +[0.0.0]: https://github.com/go2null/sshag/releases/tag/0.0.0 diff --git a/COPYING b/LICENSE similarity index 100% rename from COPYING rename to LICENSE diff --git a/README.markdown b/README.md similarity index 60% rename from README.markdown rename to README.md index b868ae5..b579d7b 100644 --- a/README.markdown +++ b/README.md @@ -1,48 +1,86 @@ -`sshag` -======= +# `sshag` ## "Socket to me, baby!" -This is a sourceable shell include file which provides a `sshag` function -to conveniently hook up with an operating ssh-agent. +This is a sourceable shell include file which provides a `sshag` function to +conveniently hook up with an operating `ssh-agent`. It will start a new agent session if it doesn't find an agent to connect with. -You might want to source it from within your `~/.bashrc` file -or other profile script. +You might want to source it from within your `~/.bashrc` file or other profile +script. -It can also be run as an executable script, -and its output stored in the relevant environment variable. -This is particularly useful -when it is desired to configure a non-shell environment, -for example that of a text editor. +It can also be run as an executable script, and its output stored in the +relevant environment variable. This is particularly useful when it is desired +to configure a non-shell environment, for example, that of a text editor. Messages are emitted on standard out; the output will always consist of just the socket location. -## Usage: -Sourced: - - $ ssh alotta@fagina.example.com - Enter passphrase for key '/home/austin/.ssh/id_dsa': ^C - $ source sshag.sh - $ sshag - Keys: - 2048 0d:db:a1:1a:cc:01:ad:ec:ab:00:d1:ed:eb:ac:1e:00 /home/austin/.ssh/id_dsa (DSA) - /tmp/ssh-5ock3tt0m3/agent.6969 - $ ssh alotta@fagina.example.com - ... - -Invoked: - - $ export SSH_AGENT_SOCK=`sshag.sh` - Output should be assigned to the environment variable $SSH_AUTH_SOCK. - Keys: - 2048 0d:db:a1:1a:cc:01:ad:ec:ab:00:d1:ed:eb:ac:1e:00 /home/austin/.ssh/id_dsa (DSA) - -Appended to `~/.bashrc`: - source ~/lib/sshag/sshag.sh; sshag &>/dev/null +## Installation + +### Recommended installation method + +Use the [pearl] package manager. +```sh +# install +pearl_conf="${XDG_CONFIG_HOME:-$HOME/.config}/pearl/pearl.conf" +if grep '^}$' >/dev/null 2>&1 "$pearl_conf"; then + sed -i.bak 's/^}$//' "$pearl_conf" +else + printf '%s\n' 'PEARL_PACKAGES = {' > "$pearl_conf" +fi +printf '"%s": {\n' 'sshag' >>"$pearl_conf" +printf '"%s": "%s"\n' \ + 'url' 'https://github.com/go2null/sshag.git' \ + >>"$pearl_conf" +printf '"%s": "%s"\n' \ + 'description' 'Hook up with an operating or new SSH agent' \ + >>"$pearl_conf" +printf '{\n' >>"$pearl_conf" +pearl install sshag + +# update +pearl update sshag +``` + +### Manual installation + +```sh +# install +wget https://raw.githubusercontent.com/go2null/sshag/stable/sshag.sh +sh sshag.sh install + +# update +sshag update + +# uninstall/remove +sshag remove +``` + +## Usage + +Sourced +```sh +$ ssh alotta@fagina.example.com +Enter passphrase for key '/home/austin/.ssh/id_ed25519': ^C +$ sshag alotta@fagina.example.com +Keys: + 256 SHA256:2TWr3x/H6eGvE+vx9Ur8uFQWBIXTBH3jT12yHBB4TJY austin@powers (ED25519) +``` + +Invoked +```sh +$ export SSH_AGENT_SOCK=$(sh ~/.local/share/lib/sshag/sshag.sh) +Output should be assigned to the environment variable SSH_AUTH_SOCK. +Keys: + 256 SHA256:2TWr3x/H6eGvE+vx9Ur8uFQWBIXTBH3jT12yHBB4TJY austin@powers (ED25519) +``` + +## History + +See [CHANGELOG.md]. ## Licensing @@ -52,7 +90,7 @@ Creative Commons Attribution-Sharealike License, so I'm attributing it to the superuser.com user [Zed]. SU currently links to [version 2.5] of the license. -A copy of [the full license] is distributed herein in the file COPYING. +A copy of [the full license] is distributed herein in the file [LICENSE]. ### The basic gist of the license @@ -90,12 +128,16 @@ A copy of [the full license] is distributed herein in the file COPYING. to others the license terms of this work. The best way to do this is with a link to this web page. -[response]: http://superuser.com/questions/141044/sharing-the-same-ssh-agent-among-multiple-login-sessions#answer-141241 + +[CHANGELOG.md]: https://github.com/go2null/sshag/blob/master/CHANGELOG.md +[LICENSE]: https://github.com/go2null/sshag/blob/master/LICENSE [Zed]: http://superuser.com/users/33648/zed -[version 2.5]: http://creativecommons.org/licenses/by-sa/2.5/ -[the full license]: http://creativecommons.org/licenses/by-sa/2.5/legalcode -[waived]: http://wiki.creativecommons.org/Frequently_Asked_Questions#Can_I_change_the_terms_of_a_CC_license_or_waive_some_of_its_conditions.3F -[public domain]: http://wiki.creativecommons.org/Public_domain [fair use]: http://wiki.creativecommons.org/Frequently_Asked_Questions#Do_Creative_Commons_licenses_affect_fair_use.2C_fair_dealing_or_other_exceptions_to_copyright.3F [moral]: http://wiki.creativecommons.org/Frequently_Asked_Questions#I_don.E2.80.99t_like_the_way_a_person_has_used_my_work_in_a_derivative_work_or_included_it_in_a_collective_work.3B_what_can_I_do.3F +[pearl]: https://github.com/pearl-core/pearl#installation +[public domain]: http://wiki.creativecommons.org/Public_domain [publicity]: http://wiki.creativecommons.org/Frequently_Asked_Questions#When_are_publicity_rights_relevant.3F +[response]: http://superuser.com/questions/141044/sharing-the-same-ssh-agent-among-multiple-login-sessions#answer-141241 +[the full license]: http://creativecommons.org/licenses/by-sa/2.5/legalcode +[version 2.5]: http://creativecommons.org/licenses/by-sa/2.5/ +[waived]: http://wiki.creativecommons.org/Frequently_Asked_Questions#Can_I_change_the_terms_of_a_CC_license_or_waive_some_of_its_conditions.3F diff --git a/pearl-config/config.sh b/pearl-config/config.sh new file mode 100644 index 0000000..86829ce --- /dev/null +++ b/pearl-config/config.sh @@ -0,0 +1,4 @@ +# shellcheck shell=sh + +# load `sshag` into current environment +. "$PEARL_PKGDIR/sshag.sh" diff --git a/pearl-config/hooks.sh b/pearl-config/hooks.sh new file mode 100644 index 0000000..ba58005 --- /dev/null +++ b/pearl-config/hooks.sh @@ -0,0 +1,9 @@ +# shellcheck shell=sh + +post_install() { + . "$PEARL_PKGDIR/pearl-config/config.sh" +} + +post_update() { + post_install +} diff --git a/sshag.sh b/sshag.sh index 4dc2c28..7820acd 100755 --- a/sshag.sh +++ b/sshag.sh @@ -1,83 +1,421 @@ -#!/bin/bash +#!/bin/sh + # acquired courtesy of -# http://superuser.com/questions/141044/sharing-the-same-ssh-agent-among-multiple-login-sessions#answer-141241 - -function sshag_findsockets { - find /tmp -uid $(id -u) -type s -name agent.\* 2>/dev/null -} - -function sshag_testsocket { - if [ ! -x "$(which ssh-add)" ] ; then - echo "ssh-add is not available; agent testing aborted" >&2 - return 1 - fi - - if [ X"$1" != X ] ; then - export SSH_AUTH_SOCK=$1 - fi - - if [ X"$SSH_AUTH_SOCK" = X ] ; then - return 2 - fi - - if [ -S $SSH_AUTH_SOCK ] ; then - ssh-add -l > /dev/null - if [ $? = 2 ] ; then - echo "Socket $SSH_AUTH_SOCK is dead! Deleting!" >&2 - rm -f $SSH_AUTH_SOCK - return 4 - else - return 0 - fi - else - echo "$SSH_AUTH_SOCK is not a socket!" >&2 - return 3 - fi -} - -function sshag_init { - # ssh agent sockets can be attached to a ssh daemon process or an - # ssh-agent process. - - AGENTFOUND=0 - - # Attempt to find and use the ssh-agent in the current environment - if sshag_testsocket ; then AGENTFOUND=1 ; fi - - # If there is no agent in the environment, search /tmp for - # possible agents to reuse before starting a fresh ssh-agent - # process. - if [ $AGENTFOUND = 0 ] ; then - for agentsocket in $(sshag_findsockets) ; do - if [ $AGENTFOUND != 0 ] ; then break ; fi - if sshag_testsocket $agentsocket ; then AGENTFOUND=1 ; fi - done - fi - - # If at this point we still haven't located an agent, it's time to - # start a new one - if [ $AGENTFOUND = 0 ] ; then - eval `ssh-agent` - fi - - # Clean up - unset AGENTFOUND - unset agentsocket - - { echo "Keys:"; ssh-add -l | sed 's/^/ /'; } >&2 - - # Display the found socket - echo $SSH_AUTH_SOCK; -} - - -# If we are not being sourced, but rather running as a subshell, -# let people know how to use the output. -if [[ $0 =~ sshag ]]; then - echo 'Output should be assigned to the environment variable $SSH_AUTH_SOCK.' >&2 - sshag_init -# Otherwise, make it convenient to invoke the search. -# When the alias is invoked, it will modify the shell environment. -else - alias sshag="sshag_init" -fi +# http://superuser.com/questions/141044/sharing-the-same-ssh-agent-among-multiple-login-sessions#answer-141241 +# Project at: https://github.com/go2null/sshag + + +# == LOAD ONCE == # + +sshag_function_is_defined() { + type sshag >/dev/null 2>&1 && return 0 || return 1 +} + +# $0 is set to filename (and any leading path) if invoked as a script. +# so if $0 does not end with the filename, then it is sourced. +# When sourced, +# POSIX - $0 is undefined +# bash - set to `*bash` +# zsh - in the main script scope - same as when called as a script. +# - within a function - name of the function, as here. +sshag_is_sourced() { + [ "${0%sshag.sh}" = "$0" ] && return 0 || return 1 +} + + +# == MAIN == # + +# Only allow to source file once. +# This simplifies the installation by adding to all the dot profiles +# and only source once. +sshag_function_is_defined && sshag_is_sourced && return 0 || : + +# USAGE +# sshag install [TARGET_DIR] - install/update +# sshag update [TARGET_DIR] - update +# sshag uninstall [TARGET_DIR] - uninstall +# sshag - start new/use existing agent +# sshag AGENT_SOCKET - use specified agent +# sshag USER@HOST [SSH_OPTIONS_AND_ARGS] - start agent and ssh to USER@HOST +sshag() { + if [ $# -gt 0 ]; then + case "$1" in + install) shift; sshag_install 'install' "$@"; return $? ;; + update) shift; sshag_install 'update' "$@"; return $? ;; + uninstall) shift; sshag_install 'remove' "$@"; return $? ;; + remove) shift; sshag_install 'remove' "$@"; return $? ;; + *) [ -e "$1" ] || { sshag_ssh "$@"; return $?; } ;; + esac + fi + + if sshag_is_sourced; then + sshag_agent_get_socket "$1" + sshag_print_or_add_keys + else + sshag_agent_print_notice + fi +} + + +# == Get/Start SSH-AGENT == # + +# $1 - optional. Agent Socket +sshag_agent_get_socket() { + # Attempt to use socket passed in + sshag_agent_vet_socket "$1" && return 0 || : + + # Attempt to use the ssh-agent in the current environment + sshag_agent_vet_socket "$SSH_AUTH_SOCK" && return 0 || : + + # If there is no agent in the environment, + # search for any agent to reuse before starting a fresh ssh-agent process. + # SSH agent sockets can be attached to an SSH daemon process + # or an ssh-agent process. + for agent_socket in $(sshag_agent_find_sockets); do + sshag_agent_vet_socket "$agent_socket" && return 0 || : + done + + # Start a new agent + sshag_agent_new_socket +} + +# $1 - optional. Agent Socket +sshag_agent_vet_socket() { + [ -n "$1" ] || return 1 + + if [ -S "$1" ]; then + export SSH_AUTH_SOCK="$1" + ssh-add -l >/dev/null 2>&1 + if [ $? -eq 2 ]; then + rm -f "$SSH_AUTH_SOCK" + print_warning "Socket '$SSH_AUTH_SOCK' is dead! Deleted!" + return 1 + fi + else + print_warning "'$SSH_AUTH_SOCK' is not a socket!" + return 1 + fi +} + +sshag_agent_find_sockets() { + # OpenSSH only uses these two dirs + for dir in '/tmp' "$TMPDIR"; do + find "$dir" -user "$(id -u)" -type s -path '*/ssh-*/agent.*' 2>/dev/null + done +} + +sshag_agent_new_socket() { + eval "$(ssh-agent)" +} + +# Ensure keys are loaded +sshag_agent_print_or_add_keys() { + if keys="$(ssh-add -l 2>/dev/null)"; then + # Display keys currently loaded in the agent + print_info "Keys:" + print_info "$(printf '* %s' "$keys")" + else + ssh-add + fi +} + +sshag_agent_print_notice() { + print_info "$(cat <<- NOTICE + + Do the following to add the ssh-agent to your current session + export SSH_AGENT_SOCK="\$(sh '$0')" + Or, simply source the file + source '$0' + NOTICE + )" +} + + +# == SSH wrapper == # + +# Load first key for specified user@hostname and start SSH. +# $1 - required. user@host +# $@ - optional. SSH options +sshag_ssh() { + # This is needed for OpenSSH before v7.2 which added support AddKeysToAgent + # Or if the local SSH client support AddKeysToAgent, + # but it is not set in the ~/.ssh/config + + # OpenSSH v7.2 added support for AddKeysToAgent. + # Honor it if it is used in ssh_config. + # Otherwise, attempt to load identityfile as user may use a common ssh_config + # on multiple machines where only some support AddKeysToAgent. + # (OpenSSH before v7.2 barfs on params it doesn't know about so can't use + # it in a common ssh_config where some machines have pre v7.2 OpenSSH.) + + # ensure ssh-agent is active + sshag + + unset ssh_opts + + user_host="$1" + shift + + if sshag_ssh_config_has_add_keys; then + # Honor AddKeysToAgent settings + : # do nothing + elif ssh -o AddKeysToAgent 2>&1 | grep 'missing argument' >/dev/null; then + # If this SSH supports AddKeyToAgent, then use it + ssh_opts='-o AddKeysToAgent=yes' + else + # This is needed for OpenSSH pre v7.2, before AddKeysToAgent was added + sshag_ssh_add_key_to_agent "$user_host" || : # let SSH emit its own error + fi + + # `$ssh_opts` may be unset, quoting it will pass an empty string to SSH + # shellcheck disable=SC2086,SC2029 + ssh "$@" $ssh_opts "$user_host" +} + +# Checks if ~/.ssh/config has AddKeysToAgent +sshag_ssh_config_has_add_keys() { + grep '^[[:blank:]]*AddKeysToAgent' \ + "$HOME/.ssh/config" "/etc/ssh/ssh_config" >/dev/null 2>&1 \ + && return 0 || return 1 +} + +# This is needed for OpenSSH before v7.2 which added support AddKeysToAgent +# Or if the local SSH client support AddKeysToAgent, +# but it is not set in the ~/.ssh/config +# $1 - required. user@host +sshag_ssh_add_key_to_agent() { + sshag_ssh_is_identity_loaded "$1" && return 0 || : + + # load identity if one is defined for the user@hostname. + if sshag_identity="$(sshag_ssh_get_identity "$1")"; then + ssh-add "$sshag_identity" && return 0 || : + fi + + print_error "Unable to load identity '$sshag_identity'!" + return 1 +} + +# $1 - required. user@host +sshag_ssh_is_identity_loaded() { + echo 'exit' | ssh -o BatchMode=yes -- "$1" 2>/dev/null && return 0 || return 1 +} + +# $1 - required. user@host +sshag_ssh_get_identity() { + sshag_identity="$(ssh -v -o BatchMode=yes "$1" 2>&1 \ + | awk ' /identity file/ { print $4 } ' \ + | head -n 1)" + + if [ -n "$sshag_identity" ]; then + sshag_identity="$(realpath -m "$sshag_identity")" + printf '%s' "$sshag_identity" + else + return 1 + fi +} + + +# == INSTALL == + +# $1 - required. action - install, update, or remove +# $2 - optional. install directory +sshag_install() ( + check_commands 'ssh' 'ssh-add' 'ssh-agent' + check_commands -require 'git' + + dir="$(sshag_install_path "$2")" + dir="${dir%/sshag}" # strip 'sshag' from path, as necessary + dir="$dir/sshag" + + if [ -d "$dir" ]; then + case "$1" in + install|update) + sshag_update "$dir" + return $? + ;; + remove) + sshag_remove "$dir" + return $? + ;; + esac + fi + + if [ "$1" = 'remove' ]; then + print_fatal "Cannot detect where 'sshag' is installed" + fi + + print_info "Installing to $dir." + sshag_install_download "$dir" + + print_info "Adding to startup files" + sshag_config=". '$dir/sshag.sh'" + sshag_install_profiles "$sshag_config" + sshag_install_manual "$sshag_config" +} + +# $1 - optional. install parent directory +# Use LIB directory specified by UAPI (SystemD's FileSystem Hierarchy successor) +# https://uapi-group.org/specifications/specs/linux_file_system_hierarchy/#locallib +sshag_install_path() { + unset dir + + if [ -n "$1" ]; then + dir="$(realpath -m "$1" 2>/dev/null)" + [ -z "$dir" ] && print_fatal " Invalid directory $1." + else + if [ "$USER" = 'root' ]; then + dir='/usr/local/lib' + + sshag_install_migrate "${XDG_DATA_DIRS%%:*}/lib" "$dir" # v3.0.0 path + sshag_install_migrate '/usr/local/share/lib' "$dir" # v3.0.0 path + else + : "${XDG_LIB_HOME:="$(realpath "${XDG_DATA_HOME:-"$HOME/.local/share"}/../lib")"}" + dir="$XDG_LIB_HOME" + + sshag_install_migrate "$XDG_DATA_HOME/../lib" "$dir" # v1.3.0 path + sshag_install_migrate "$HOME/.local/lib" "$dir" # v2.0.0 path + sshag_install_migrate "$XDG_DATA_HOME/lib" "$dir" # v3.0.0 path + fi + fi + + [ -d "$dir" ] || mkdir -p "$dir" || print_fatal " Cannot create directory '$dir'." + printf '%s' "$dir" +} + +# $1 - from path +# $2 - to path +sshag_install_migrate() { + [ -d "$1/sshag" ] || return 0 + + [ "$(realpath "$1")" = "$(realpath "$2")" ] && return 0 || : + + print_info ' Migrating previous installation' + print_info " from $1/sshag" + print_info " to $2" + + mkdir -p "$2" + mv "$1/sshag" "$2/" +} + +# $1 - required. install directory +sshag_install_download() { + git clone 'https://github.com/go2null/sshag.git' "$1" \ + || print_fatal " 'git clone' failed with above error." +} + +# add to shell startup files +# $1 - required. sshag config line +sshag_install_profiles() { + if [ "$USER" = 'root' ]; then + if touch '/etc/profile.d/sshag.sh' 2>/dev/null; then + sshag_install_profile "$1" '/etc/profile.d/sshag.sh' + else + sshag_install_profile "$1" '/etc/profile' + fi + else + sshag_install_profile "$1" "$HOME/.profile" + sshag_install_profile "$1" "$HOME/.bash_profile" + sshag_install_profile "$1" "$HOME/.bashrc" + sshag_install_profile "$1" "$HOME/.zshrc" + fi +} + +# $1 - required. config line +# $2 - required. config file +sshag_install_profile() { + [ -w "$2" ] || return 1 + + if grep "^[ \t]*$1" "$2" >/dev/null; then + print_info " SKIPPED $2, already added." + return 0 + fi + + print_line "$1" >> "$2" + print_info " ADDED to $2." +} + +# $1 - required. config line +sshag_install_manual() { + print_info "Add the following to any additional shell startup files" + print_info " $1" +} + +# $1 - required. install directory +sshag_update() { + print_info "Updating 'sshag' at '$1'." + cd "$1" || print_fatal " Cannot access '$1'." + git pull +} + +# $1 - required. install directory +sshag_remove() { + print_info "Removing 'sshag' at '$1'." + rm -rf "$1" + + print_info "Removing from startup files" + sshag_remove_profiles +} + +sshag_remove_profiles() { + file='/etc/profile.d/sshag.sh' + [ -w "$file" ] && rm "$file" && print_info " REMOVED '$file'." + + files="/etc/profile +$HOME/.profile +$HOME/.bash_profile +$HOME/.bashrc +$HOME/.zshrc" + + while IFS='' read -r file; do + sshag_remove_profile "$file" + done <<- EOF + $files + EOF +} + +# $1 - required. config file +sshag_remove_profile() { + [ -e "$1" ] || return 0 + grep 'sshag.sh' "$1" 1>/dev/null 2>&1 || return 0 + + print_info " $1" + [ ! -w "$1" ] && print_warning " SKIPPED, cannot edit file." && return 0 + + sed -i.bak '/.*sshag.sh.*/ d' "$1" \ + || print_warning " FAILED to remove from file." +} + + +# == HELPERS == # + +# $1 - -require. OPTIONAL +check_commands() { + [ "$1" = '-require' ] && { require=1; shift; } || require='' + + for cmd in "$@"; do + if [ ! -x "$(command -v "$cmd")" ]; then + if [ -n "$require" ]; then + print_fatal "'$cmd' is not available! Aborting!" + else + print_warning "'$cmd' is not available! Please install it." + fi + fi + done +} + +print_line() { printf '%s\n' "$*"; } + +print_stderr() { print_line "$@" >&2; } +# Do not send messages to 'stdout' +# - it is reserved for outputting $SSH_AUTH_SOCH when invoked in a subshell + +print_debug() { print_stderr "DEBUG: $*"; return 0; } +print_error() { print_stderr "ERROR: $*"; return 0; } +print_fatal() { print_stderr "FATAL: $*"; exit 1; } +print_info() { print_stderr "INFO: $*"; return 0; } +print_warning() { print_stderr "WARNING: $*"; return 0; } + + +# == HOOK == + +sshag "$@"