4

I'm a beginner at Bash. I've previously done a hang the man program in Python and trying to do a similar program with Bash but I'm stuck at the moment. I'm trying to replace a character of the "secret" with the found letter at the correct index. I haven't found anything online that fully helped me.

Example:

word="abcdef"
secret="______"

If guess is find at index in word then:

guess="a"
Outcome: a_____
guess="e"
Outcome: a___e_

I came pretty close with:

echo $secret | sed s/./$guess/$i

but this didn't work when index was 0.

sed: -e expression #1, char 7: number option to `s' command may not be zero
tripleee
  • 175,061
  • 34
  • 275
  • 318
chrimol
  • 41
  • 1
  • 1
    There is no 0th match in `s///1` etc. Like line numbers that start at 1, match numbers start at 1 as well. So, why not just increment your index before calling sed? – stevesliva Sep 05 '20 at 13:54
  • @stevesliva Thanks man! I don't know why I didn't try that before. – chrimol Sep 05 '20 at 16:27

2 Answers2

3

Bash isn't particularly good for this sort of program, but it certainly has string substitution built in.

Maybe keep the correct guesses so far in $correct and replace all the rest.

echo "${secret//[!$correct]/_}"

The parameter expansion ${secret//pattern/replacement} prodcuces the value of $secret with every match on pattern substituted with replacement; the pattern [!abc] matches any single character which isn't a, b, or c. (With a single slash before the pattern, only the first occurrence gets replaced.) For this to work, $correct can't be empty, so maybe initialize it to "_" at the start of the program.

If you really want index-based string handling, ${variable:start:len} extracts a substring from $variable.

These parameter expansions are Bash-specific; POSIX sh supports a much smaller set of operations.

tripleee
  • 175,061
  • 34
  • 275
  • 318
2

To be clear, the part you're asking about isn't something you'd use bash or any other shell for so if you're trying to learn the right way to do things in a UNIX environment: A shell is an environment from which to create/destroy files and processes, and to sequence calls to tools. If you want to do something like this using mandatory UNIX tools (i.e. the tools that exist on every UNIX box) then you'd use awk, not shell. The only shell part would be to call awk.

Given that, if you want to write a hangman program in "bash" here is one way you could do the prompting/guessing part using any awk in any shell on every UNIX system:

$ cat tst.awk
BEGIN {
    secret = word
    gsub(/./,"_",secret)
    print "\nOutcome:", secret
    printf "guess? "
}
{
    guess = $0
    while ( pos=index(word,guess) ) {
        secret = substr(secret,1,pos-1) guess substr(secret,pos+1)
        word   = substr(word,1,pos-1)    "_"  substr(word,pos+1)
    }
    print "\nOutcome:", secret
    if (word ~ /^_*$/) {
        exit
    }
    printf "guess? "
}

and now the bash (or any other shell) part would just be:

$ awk -v word='abcdef' -f tst.awk

Outcome: ______
guess? a

Outcome: a_____
guess? e

Outcome: a___e_
guess? d

Outcome: a__de_
guess? f

Outcome: a__def
guess? b

Outcome: ab_def
guess? c

Outcome: abcdef

I decided to implement a full hangman shell script that generates words of 5 letters or more for you to guess as it was kinda fun to play so here you go:

$ cat ./hangman.sh
#!/usr/bin/env bash

declare -a words
trap 'printf "\nwords used:\n"; printf "%s\n" "${words[@]}"; exit' 0

prtwords() {
    local dfltwordfile='/usr/share/dict/words'

    {
        if [[ -s "$dfltwordfile" ]]; then
            cat "$dfltwordfile"
        else
            curl -s 'https://svnweb.freebsd.org/csrg/share/dict/words?view=co&content-type=text/plain'
        fi
    } |
    tr 'A-Z' 'a-z' |
    grep -E '.{5}' |
    shuf
}

guesschars() {
    awk -v word="$1" '
        BEGIN {
            secret = origWord = word
            gsub(/./,"_",secret)
            print "\nOutcome:", secret
            printf "guess? "
            maxFailures = 6
        }
        NF {
            guess = substr($1,1,1)
            isFailure = 1
            while ( pos=index(word,guess) ) {
                isFailure = 0
                secret = substr(secret,1,pos-1) guess substr(secret,pos+1)
                word   = substr(word,1,pos-1)    "_"  substr(word,pos+1)
            }
            numFailures += isFailure
            print "\nOutcome:", secret
            if ( (word ~ /^_*$/) || (numFailures == maxFailures) ) {
                print "The word was", origWord
                exit
            }
            printf "guess? "
        }
    '
}

# See https://stackoverflow.com/a/41652573/1745001 for rationale on
# the file descriptor manipulation below.

exec 3</dev/tty || exec 3<&0            ## make FD 3 point to the TTY or stdin (as fallback)

echo 'Hit interrupt to stop' >&2
while IFS= read -r word; do             ## |- loop over lines read from FD 0
    words+=( "$word" )
    guesschars "$word" <&3              ## |- run guesschars with its stdin copied from FD 3
    echo '#####' >&2
done < <(prtwords)                      ## \-> ...while the loop is run with prtwords output on FD 0

exec 3<&-                               ## close FD 3 when done.

.

$ ./hangman.sh
Hit interrupt to stop

Outcome: __________
guess? a

Outcome: _______a__
guess? e

Outcome: _e__e__a_e
guess? s

Outcome: _e__e__a_e
guess? t

Outcome: _e__e__ate
guess? r

Outcome: _e_re__ate
guess? c

Outcome: _e_rec_ate
guess? d

Outcome: de_rec_ate
guess? p

Outcome: deprec_ate
guess? i

Outcome: depreciate
The word was depreciate
#####

Outcome: ______
guess?
words used:
depreciate
enrico
Ed Morton
  • 188,023
  • 17
  • 78
  • 185
  • 1
    very nicely done - thanks for sharing. You have a mismatching `'` in your `trap` definition. It should be `printf "%s\n"` - otherwiise getting ````unexpected EOF while looking for matching `"'```` – vgersh99 Sep 07 '20 at 15:50
  • 1
    @vgersh99 so I did! thanks for catching it, fixed now. – Ed Morton Sep 07 '20 at 15:51
  • 1
    This is addictive! and inspiring too. I don't know why is this a feature for awk, but for empty guess, `index(word,"")` returns `1` (!), so the while loop goes forever, so `guess` should always have something. – thanasisp Sep 08 '20 at 03:52
  • 1
    @thanasisp You're right, I've never noticed that before. I dont want to spend any time trying to make this script really robust (a useful exercise for anyone else who wants to do so!) but since this one was simple - I tweaked the code to protect against that and someone entering a multi-char string (just takes the first char). – Ed Morton Sep 08 '20 at 14:46
  • Thanks for your answer! Yes, I know Bash is not really made for this kind of program. It was just a thing I wanted to do so I could learn while doing it. – chrimol Sep 10 '20 at 09:59