#!/usr/bin/env -S bash -e # # Copyright 2021 Nathan L. Conrad # # This program is free software: you can redistribute it and/or modify it under # the terms of version 3 of the GNU General Public License as published by the # Free Software Foundation. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # Establish usage IFS= read -rd '' usage << '__eof__' || true Usage: sockhop [-chil] [--] sock [[cmd ...] | [arg ...]] Transport and execute shell commands via Unix domain sockets Optional arguments: -c, --clean Imply -l/--listen, ignore SOCKHOP_SEPARATOR, and clear the terminal before execution of each command received from the socket. Incompatible with -i/--stdin. -h, --help Show this help message and exit -i, --stdin Read standard input lines and send them to the socket. Implied if -c/--clean and -l/--listen are not set and the standard input file descriptor is not opened on a terminal. Incompatible with -c/--clean and -l/--listen. Commands supplied as positional arguments after the socket file are sent to the socket before attempting to read lines from standard input. -l, --listen Create the socket file, accept connections, and execute commands received from the socket in the current context. Implied if -c/clean is set or if -i/--stdin is not set, the socket file is the last argument, and the standard input file descriptor is opened on a terminal. Incompatible with -i/--stdin. To preserve execution order, only a single connection is allowed at any given time. Positional arguments after the socket file assign the values of positional parameters for executed commands, starting with $1. -- Explicitly denote the end of optional arguments Positional arguments: sock The Unix domain socket file cmd If -c/--clean and -l/--listen are not set, a command which is written to the socket. These commands are sent before attempting to read lines from standard input if -i/--stdin is set or implied. arg If -c/--clean or -l/--listen is set, the value of a positional parameter for executed commands, starting with $1 Environment variables: SOCKHOP_SEPARATOR If -c/--clean is not set, but -l/--listen is set or implied, a command which is executed between commands received from the socket to separate output of the received commands. Defaults to ':' if unset or null. SOCKHOP_SHELL If -c/--clean is set or -l/--listen is set or implied, the shell used to execute commands. Defaults to $SHELL if unset or null, '/bin/sh' if SHELL is unset or null. SOCKHOP_SHELL_ENV If -c/--clean is set or -l/--listen is set or implied, variable assignments which are expanded when SOCKHOP_SHELL is invoked to assign the values of variables for executed commands SOCKHOP_SHELL_NAME If -c/--clean is set or -l/--listen is set or implied, the shell name supplied when SOCKHOP_SHELL is invoked, which is used in warning and error messages. If unset, defaults to the basename of SOCKHOP_SHELL. SOCKHOP_SHELL_OPTS If -c/--clean is set or -l/--listen is set or implied, optional arguments which are expanded when SOCKHOP_SHELL is invoked to set the shell options for executed commands. Defaults to '-e' if unset. SOCKHOP_SOCK If -c/--clean is set or -l/--listen is set or implied, a variable which is assigned the value of the socket file argument in the environment of executed commands __eof__ # Usage: argerr msg # # Write an argument usage error message to standard error and fail # # Positional arguments: # msg The core error message function argerr { echo "$1. Use -h for help." >&2 exit 1 } # Parse optional arguments clean=0 stdin=0 listen=0 while (( $# )) do # If the argument is a single hyphen followed by multiple single-character # options, separate the first option from the argument and leave the rest # for the next iteration (( ${#1} > 2 )) && [[ $1 =~ ^-[^-]*$ ]] && \ set -- "${1::2}" "-${1:2}" "${@:2}" # Handle the argument case $1 in -c|--clean) (( stdin )) && argerr "$1 is incompatible with -i/--stdin" clean=1 listen=1 ;; -h|--help) echo -n "$usage" exit 0 ;; -i|--stdin) (( clean )) && argerr "$1 is incompatible with -c/--clean" (( listen )) && argerr "$1 is incompatible with -l/--listen" stdin=1 ;; -l|--listen) (( stdin )) && argerr "$1 is incompatible with -i/--stdin" listen=1 ;; --) shift break ;; -*|--*) argerr "Invalid argument '$1'" ;; *) break esac # Advance to the next argument shift done # Pop the socket file argument (( $# )) || argerr 'Missing socket file argument' sock=$1 shift # Imply -l/--listen if the standard input file descriptor is opened on a # terminal, -i/--stdin is not set, and the socket file is the last argument. # Imply -i/--stdin if the standard input file descriptor is not opened on a # terminal and -l/--listen is not set. if [ -t 0 ] then (( stdin || $# )) || listen=1 else (( listen )) || stdin=1 fi # Either listen, receive, and execute or connect and send if (( listen )) then # There is nothing to separate until a command is executed separator_required=0 # Default the shell, its environment, and options used to execute commands shell_env="SOCKHOP_SOCK='$sock'" [[ -n $SOCKHOP_ENV ]] && shell_env="$SOCKHOP_ENV $env" shell=${SOCKHOP_SHELL:-${SHELL:-/bin/sh}} shell_name=${SOCKHOP_SHELL_NAME-$(basename -- "$shell")} shell="$shell_env '$shell' ${SOCKHOP_SHELL_OPTS--e} -c --" set -- "$shell_name" "$@" # Default the separator command separator=${SOCKHOP_SEPARATOR:-:} # Execute received commands until listening fails while true do while read -r cmd do [[ $cmd = __fail__ ]] && exit 1 if (( clean )) then echo -n $'\e[3J' && clear elif (( separator_required )) then env -S "$shell" "$separator" "$@" || true else separator_required=1 fi env -S "$shell" "$cmd" "$@" || true done < <(socat -u "unix-listen:$sock" stdout || echo __fail__) done else # Send positional parameters then, if applicable, standard input (( $# )) && printf -v cmds '%s\n' "$@" || cmds= (( stdin )) && arg=- || arg= cat <(echo -n "$cmds") $arg | socat -u stdin "unix-connect:$sock" fi