Bash - 如何替换特定索引处(包括索引零)的字符

4
我是Bash的初学者。之前我用Python编写了一个“猜单词”程序,现在想使用Bash编写类似的程序,但目前卡住了。我正在尝试将“秘密单词”中正确索引位置的字母替换为找到的字母。我在网上没有找到完全帮助我的东西。
示例:
word="abcdef"
secret="______"

如果在单词中找到猜测字符的索引,则执行以下操作:

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

我做得相当接近:

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

但是当索引为0时,这并没有起作用。

sed: -e expression #1, char 7: number option to `s' command may not be zero

1
s///1等中没有第0个匹配。就像从1开始的行号一样,匹配号也从1开始。所以,在调用sed之前为什么不先增加索引呢? - stevesliva
@stevesliva 谢谢你!我不知道为什么以前没有尝试过。 - chrimol
2个回答

3

Bash不是特别适合这种程序,但它确实内置了字符串替换功能。

也许可以将到目前为止正确的猜测保存在$correct中,并替换所有其余内容。

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

参数扩展${secret//pattern/replacement}会将$secret中与pattern相匹配的每一个部分都替换为replacement。其中,[!abc]模式匹配不是abc的任意单个字符。(在模式前放置一个斜杠,只有第一次出现的字符会被替换。) 为了使其正常工作,$correct不能是空的,因此可能需要在程序开始时将其初始化为"_"

如果您确实需要索引为基础的字符串处理,${variable:start:len}可以从$variable中提取子字符串。

这些参数扩展是Bash特有的;POSIX sh支持的操作集要小得多。

2
要明确的是,你所询问的部分不是使用bash或任何其他shell的内容,因此如果你想学习在UNIX环境下正确的操作方式:shell是一个环境,用于创建/销毁文件和进程,并对工具进行顺序调用。如果你想使用强制性UNIX工具(即存在于每个UNIX系统上的工具)来完成类似的操作,则应该使用awk而非shell。唯一与shell有关的部分就是调用awk。
考虑到这一点,如果你想在"bash"中编写一个hangman程序,以下是一种使用任何shell中的任何awk进行提示/猜测的方法,适用于每个UNIX系统:
$ 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? "
}

现在,bash(或任何其他shell)部分只需:

$ 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

我决定编写一个完整的hangman shell脚本,为您生成5个或更多字母的单词供您猜测,因为这很有趣,所以这里是:

$ 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://dev59.com/MJ7ha4cB1Zd3GeqPfjTt#41652573 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

1
做得非常好 - 谢谢分享。您在trap定义中有一个不匹配的 '。它应该是printf "%s\n" - 否则会出现unexpected EOF while looking for matching `"' - vgersh99
1
@vgersh99,是的,我已经修复了!感谢你的提醒。 - Ed Morton
1
这真是让人上瘾!同时也很鼓舞人心。我不知道为什么这是 awk 的一个特性,但是对于空猜测,index(word,"") 返回 1(!),所以 while 循环会一直进行下去,因此 guess 应该始终有内容。 - thanasisp
1
@thanasisp 你说得对,我以前从没注意过这个问题。我不想花时间去尝试让这个脚本变得非常健壮(这对于其他想要这样做的人来说是一个有用的练习!),但由于这个脚本很简单 - 我调整了代码以防止出现多字符字符串(只取第一个字符)。 - Ed Morton
谢谢你的回答!是的,我知道Bash并不是为这种程序而设计的。只是我想做这件事,以便在实践中学习。 - chrimol

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接