What's Good
The script shows solid understanding of several advanced bash concepts:
- Named pipes for bidirectional communication with
telnet
- Proper cleanup with
trap ... EXIT to remove temporary files
- Background process management tracking PIDs for cleanup
stdbuf to control buffering - good awareness of the problem space
printf '%s\n' over echo for reliable output
- Nice level of commenting
The overall structure is readable and the intent is clear.
Issues to Address
Missing Shebang
Every script needs a shebang line to specify the interpreter. The best practice way is to do:
#!/usr/bin/env bash
You will also see this:
#!/bin/bash
which doesn't rely on the PATH to find bash.
Without it, the script's behavior depends on which shell invokes it.
The exec #>&- Typo
This line appears broken:
exec #>&-
The # starts a comment, so this does nothing. You probably meant:
exec 3>&-
This would close file descriptor 3. However, you don't actually want to close it here - tee needs to write to $FIFO_IN which the telnet process is still reading from.
The Performance Bug
The lag you're experiencing likely stems from this line:
tee "$FIFO_IN" > /dev/null
Each time you type a command, tee opens and writes to $FIFO_IN. But since
it's a FIFO, not a regular file, this creates blocking behavior. The telnet
process reading from the FIFO may not be consuming data fast enough, or
there's buffering accumulating somewhere in the pipeline.
A cleaner approach: keep using file descriptor 3 that's already connected:
while IFS= read -r line; do
printf '%s\n' "$line" >&3
done
Or redirect stdin directly to the existing FD:
cat >&3
No Error Handling
What happens if:
mkfifo fails?
telnet can't connect?
- The server disconnects unexpectedly?
The script will blindly proceed, sending commands into the void.
Hardcoded Credentials
printf '%s\n' "connect Chip01 Password" >&3
Credentials in scripts are a security concern. Consider environment variables or a config file with appropriate permissions. Tools like 1Password provide command line mechanisms for accessing secure credentials.
If all of the devices have hard-coded credentials I would leave it the way you did it.
Process Cleanup Could Fail Silently
kill $TELNET_PID
kill $CAT_PID
If these processes already exited, kill will error. Also, kill without a
signal sends SIGTERM, which is fine, but checking if processes exist first is
more robust.
Rewritten Version
Here's a version with error handling that passes ShellCheck:
#!/bin/bash
#
# mud-connect.sh - Automate MUD login with pre-written commands,
# then switch to interactive mode.
#
# Usage: ./mud-connect.sh [server] [port]
set -euo pipefail # Exit on error, undefined vars, pipe failures
# Configuration - override with environment variables
readonly SERVER="${MUD_SERVER:-SERVER_ADDRESS}"
readonly PORT="${MUD_PORT:-PORT_NUMBER}"
readonly USERNAME="${MUD_USER:-Chip01}"
readonly PASSWORD="${MUD_PASS:-Password}"
# Cleanup function for trap
cleanup() {
local exit_code=$?
# Close file descriptor if open
exec 3>&- 2>/dev/null || true
# Kill background processes if they exist
if [[ -n "${TELNET_PID:-}" ]] && kill -0 "$TELNET_PID" 2>/dev/null; then
kill "$TELNET_PID" 2>/dev/null || true
fi
if [[ -n "${CAT_PID:-}" ]] && kill -0 "$CAT_PID" 2>/dev/null; then
kill "$CAT_PID" 2>/dev/null || true
fi
# Remove FIFOs
rm -f "$FIFO_IN" "$FIFO_OUT"
exit "$exit_code"
}
# Send a command to the MUD with optional delay
send_cmd() {
local cmd="$1"
local delay="${2:-1}"
printf '%s\n' "$cmd" >&3
sleep "$delay"
}
# Validate dependencies
check_dependencies() {
local missing=()
for cmd in telnet mkfifo stdbuf; do
if ! command -v "$cmd" &>/dev/null; then
missing+=("$cmd")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
echo "Error: Missing required commands: ${missing[*]}" >&2
exit 1
fi
}
# Main script
main() {
check_dependencies
echo "Creating communication pipes..."
FIFO_IN=$(mktemp -u)
FIFO_OUT=$(mktemp -u)
if ! mkfifo "$FIFO_IN" || ! mkfifo "$FIFO_OUT"; then
echo "Error: Failed to create FIFOs" >&2
exit 1
fi
trap cleanup EXIT INT TERM
echo "Connecting to $SERVER:$PORT..."
stdbuf -oL -i0 telnet "$SERVER" "$PORT" < "$FIFO_IN" > "$FIFO_OUT" 2>&1 &
TELNET_PID=$!
# Verify telnet started
sleep 1
if ! kill -0 "$TELNET_PID" 2>/dev/null; then
echo "Error: Telnet failed to start" >&2
exit 1
fi
# Open write end of input FIFO
exec 3> "$FIFO_IN"
# Display server output in background
cat "$FIFO_OUT" &
CAT_PID=$!
# Wait for connection to establish
echo "Waiting for connection..."
sleep 3
# Send login sequence
echo "Logging in..."
send_cmd "connect $USERNAME $PASSWORD" 1
send_cmd "inventory" 2
send_cmd "go north east north north west" 3
echo "Done sending pre-written commands."
echo "Switching to interactive mode (Ctrl+C to exit)..."
echo ""
# Interactive mode - forward stdin to telnet
# Using cat instead of tee since we don't need to duplicate output
cat >&3
}
main "$@"
Key Improvements
- Shebang and strict mode -
set -euo pipefail catches many errors early
- Robust cleanup - Checks if processes exist before killing, handles
signals properly
- Dependency checking - Fails fast with clear message if tools are missing
- Configuration via environment - No hardcoded credentials in the script
- Helper function -
send_cmd reduces duplication and makes delays
explicit
- Error messages to stderr - Using
>&2 so errors don't mix with output
- Comments - Explains the non-obvious parts
- Passes ShellCheck - No warnings or errors
expect\$\endgroup\$