6
\$\begingroup\$

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 :)

New contributor
Chip01 is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
\$\endgroup\$
4
  • 1
    \$\begingroup\$ Do you care about error handling? \$\endgroup\$ Commented Nov 19 at 16:27
  • \$\begingroup\$ Apparently I dont need to, I never got any errors. But it would be nice to know where to implement some handling \$\endgroup\$ Commented Nov 19 at 16:35
  • 3
    \$\begingroup\$ Sounds like you're reinventing expect \$\endgroup\$ Commented 2 days ago
  • \$\begingroup\$ Now that you mention it \$\endgroup\$ Commented 2 days ago

1 Answer 1

6
\$\begingroup\$

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

  1. Shebang and strict mode - set -euo pipefail catches many errors early
  2. Robust cleanup - Checks if processes exist before killing, handles signals properly
  3. Dependency checking - Fails fast with clear message if tools are missing
  4. Configuration via environment - No hardcoded credentials in the script
  5. Helper function - send_cmd reduces duplication and makes delays explicit
  6. Error messages to stderr - Using >&2 so errors don't mix with output
  7. Comments - Explains the non-obvious parts
  8. Passes ShellCheck - No warnings or errors
\$\endgroup\$
1
  • \$\begingroup\$ Hello! Just wanted to say thanks for the amazing answer. Also, you were right about how the exec #>&- was supposed to be exec 3>&-, but the reason why I wanted exec 3>&- in the first place was so that the write end of input $FIFO_IN does not have two simultaneous opened "sessions" because of thetee that follows. Is this correct? \$\endgroup\$ Commented yesterday

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.