I made this executable because I used to play on a MUD, which was essentially a server to which you connect to through telnet to play in a multiplayer world. What annoyed me was how difficult it was to remember the automatically generated password that comes with your account and how repetitive the routine commands were.
Hence why I made a shell script that starts off as any other automatic telnet (or ssh, if you choose so) networking tool, sending pre-written commands to a server with set delays in between of each command and live feedback from the server, but after sending these pre-written commands, it starts listening to the user's input and acts as if it were a regular telnet connection from there on. Here is the script:
echo "Now connecting..."
FIFO_IN=$(mktemp -u)
FIFO_OUT=$(mktemp -u)
mkfifo "$FIFO_IN"
mkfifo "$FIFO_OUT"
trap 'rm -f "$FIFO_IN" "$FIFO_OUT"' EXIT
# Connect telnet to the pipes
stdbuf -oL -i0 'telnet' 'SERVER_ADDRESS' 'PORT_NUMBER' < "$FIFO_IN" > "$FIFO_OUT" &
TELNET_PID=$!
echo "Connect..."
# Wait for telnet to be established
sleep 3
exec 3> "$FIFO_IN"
cat "$FIFO_OUT" &
CAT_PID=$!
echo "Logging in..."
printf '%s\n' "connect Chip01 Password" >&3
sleep 1
printf '%s\n' "inventory" >&3
sleep 2
printf '%s\n' "go north east north north west" >&3
sleep 3
# More pre-written commands may follow...
echo "Done sending pre-written commands."
echo "Now switching from pre-written commands to terminal input..."
# Closing first session before opening the new one for the terminal input
exec #>&-
# Connect terminal input to telnet input
tee "$FIFO_IN" > /dev/null
# When tee exits (Ctrl+C), kill the other processes
kill $TELNET_PID
kill $CAT_PID
So it worked pretty well, but I noticed over time that the more commands I entered a while after the automatic ones were sent, the more I experience lag. I tried a regular connection using the telnet command, and it never happened. Then once more I tried with the executable, and it gradually started lagging for each command I enter.
Its nothing bad, and as long as you enter under 100 commands you dont experience any visible differences, but I have a hunch that it might be buffering the commands I enter somewhere, and in a bad way, slowing down the performance.
If you have any idea for why it does so, or any ideas on things I should try, please tell me :)
1 Answer 1
What's Good
The script shows solid understanding of several advanced bash concepts:
- Named pipes for bidirectional communication with
telnet - Proper cleanup with
trap ... EXITto remove temporary files - Background process management tracking PIDs for cleanup
stdbufto control buffering - good awareness of the problem spaceprintf '%s\n'overechofor 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:
mkfifofails?telnetcan'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 pipefailcatches 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_cmdreduces duplication and makes delays explicit - Error messages to stderr - Using
>&2so errors don't mix with output - Comments - Explains the non-obvious parts
- Passes ShellCheck - No warnings or errors
-
\$\begingroup\$ Hello! Just wanted to say thanks for the amazing answer. Also, you were right about how the
exec #>&-was supposed to beexec 3>&-, but the reason why I wantedexec 3>&-in the first place was so that the write end of input$FIFO_INdoes not have two simultaneous opened "sessions" because of theteethat follows. Is this correct? \$\endgroup\$Chip01– Chip012025年11月21日 17:57:25 +00:00Commented Nov 21 at 17:57 -
1\$\begingroup\$ Since
execis replacing the contents of the current process, there aren't two simultaneous process from the FD perspective. \$\endgroup\$chicks– chicks2025年11月25日 05:29:32 +00:00Commented Nov 25 at 5:29 -
\$\begingroup\$ Would there be any risk or problem if the
execwas taken off? \$\endgroup\$Chip01– Chip012025年11月25日 12:23:37 +00:00Commented Nov 25 at 12:23 -
1\$\begingroup\$ Removing the
execto create the FIFO would break things. Removing the cleanupexecwouldn't matter since this whole process will exit soon and the OS will clean it all up for us. \$\endgroup\$chicks– chicks2025年11月25日 14:26:19 +00:00Commented Nov 25 at 14:26
expect\$\endgroup\$