You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

193 lines
7.2 KiB
Bash

#!/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