使用sed处理passwd文件

使用sed处理passwd文件

我正在尝试学习 sed,但遇到了很多麻烦。我想要做的是使用带有 sed 命令的 bash 脚本处理我的 passwd 文件,以执行以下操作:对于组 ID 为 20000 的每个用户,将文件中的 GID 替换为 2000x,其中 x 是中第一个字母的顺序用户的用户名(即:a 为 1,b 为 2 等)。此外,对于默认 shell 为 bash 的每个用户,将其组更改为 bash,对于使用 shell tcsh 的用户,将其组更改为 tcshgroup。我已经在 awk 中完成了上述工作(我发现使用起来更容易),但我什至不知道从哪里开始使用 sed。任何帮助深表感谢。

这是 passwd 文件的一部分:

speech-dispatcher:x:108:29:Speech Dispatcher,,,:/var/run/speech-dispatcher:/bin/sh
colord:x:109:117:colord colour management daemon,,,:/var/lib/colord:/bin/false
lightdm:x:110:118:Light Display Manager:/var/lib/lightdm:/bin/false
avahi:x:111:120:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/bin/false
hplip:x:112:7:HPLIP system user,,,:/var/run/hplip:/bin/false
pulse:x:113:121:PulseAudio daemon,,,:/var/run/pulse:/bin/false
saned:x:114:20000::/home/saned:/bin/tcsh
mmccormick:x:1000:20000:owner,,,:/home/mmccormick:/bin/bash

理想情况下,我会选择每行的字段 4 来获取 shell 的组 ID 和字段 7,但同样,我不知道在 sed 中执行此操作的方法。提前致谢。

答案1

awk 确实是这里的自然工具:/etc/passwd由冒号分隔的字段组成,每行具有相同的布局,这正是 awk 构建的解析目的。

如果您想使用 sed,基本思想是捕获括号组中的每个字段,并使用反向引用来引用每个字段的内容。例如,以下是如何将用户的 shell 更改为 zsh(以前是 bash)。

sed 's~^\([^:]*\):\([^:]*\):\([^:]*\):\([^:]*\):\([^:]*\):\([^:]*\):/bin/bash$~\1:\2:\3:\4:\5:\6:/bin/zsh~'

我用作~分隔符;通常使用,但是您可以使用其他字符,并且当出现在模式中/时使用不同的字符更方便。或或是常见的选择;为了您的理智,不要选择在正则表达式中具有特殊含义的字符。/~#!

该正则表达式包含 6 次\([^:]*\):,它匹配一个字段(除 之外的字符序列:)和一个字段分隔符。为了方便起见,我将每个字段放在单独的组中。由于前 6 个字段不会更改,因此我可以将它们全部放在一个组中,甚至最后一个字段的开头也不会更改。

sed 's~^\([^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:/bin/\)bash$~\1zsh~'

另外,由于字段个数是固定的,shell是最后一个字段,我们不需要统计字段个数。所以我们可以用更简单但不太清晰的方式编写这个程序:

sed 's~^\(.*:/bin/\)bash$~\1zsh~'

或者我们可以删除^并仅替换该行的最后一部分。

sed 's~:/bin/bash$~:/bin/zsh~'

请注意,这种临时简化可以使正则表达式更清晰,同时使意图不那么明显。

当您需要对符合某些条件的线路进行操作时,有两种基本方法。一种是匹配整条线并使用分组将其分成几部分,就像我们上面所做的那样。另一种方法是将s命令限制为与特定模式匹配的行。当条件与模式替换不直接相关时,第二种方法更具可读性。下面是基于此原则的示例:对于默认 shell 为 bash 的每个用户,将其组更改为 2981。

sed '/:\/bin\/bash$/ s~^\([^:]*\):\([^:]*\):\([^:]*\):\([^:]*\):\([^:]*\):\([^:]*\):\([^:]*\)$~\1:\2:\3:2981:\5:\6:\7'

如果要进行多个替换,则可以使用多个命令:将每个命令作为参数传递给选项-e。 (大多数 sed 实现还允许使用换行符分隔命令的单个参数;其中一些还允许分号作为命令分隔符。)请注意,这些命令依次应用于每一行,因此第一个命令的结果与第二个命令相匹配。

sed -e '/:\/bin\/bash$/ s~^\([^:]*\):\([^:]*\):\([^:]*\):\([^:]*\):\([^:]*\):\([^:]*\):\([^:]*\)$~\1:\2:\3:2981:\5:\6:\7' \
    -e '/:\/bin\/tcsh$/ s~^\([^:]*\):\([^:]*\):\([^:]*\):\([^:]*\):\([^:]*\):\([^:]*\):\([^:]*\)$~\1:\2:\3:1989:\5:\6:\7' \

答案2

@Gilles 有很多好话要说。以下是一些其他注意事项:

sed是一个流编辑器 (s)stream(ed)itor。阅读介绍部分 维基百科。重要的部分是这样的模式空间ETC。

在大多数情况下,我假设您了解正则表达式,并且不会对此进行详细介绍。


这变得有点长,但是还好。

“简单的”与shell相比,部分是为用户替换了GID。这是第一部分。更有趣的部分是翻译帐户/用户名的第一个字母并将其填充到 GID。那将是第二部分 sed - 查找表下面——结束于清单 6其中有一个或多或少的功能程序GID 中的字母到数字

其中很多可能看起来“为什么哦为什么?”– 但这是一次很好的概念训练(缺乏更好的词)。


第 1 部分:通过 shell 交换 GID

您可以添加一个函数来获取命名组的 GID,这里使用 sed 而不是 cut、IFS 或其他“更简单”的方法:

#!/bin/bash

get_gnr()
{
    # -n    Do not print unless I say so.
    # s///  Substitute lines beginning with argv 1:
    # p     Print if there was a substitution.
    # $1    Arg 1 to bash function.
    sed -n 's/^'$1':[^:]*:\(.*\):/\1/p' /etc/group
}

# Assign what ever get_gnr() prints to gr_pulse
gnr_bash=$(get_gnr "bash")
gnr_tcsh=$(get_gnr "tcsh")

printf "Group %5s = %d\n" "bash" "$gnr_bash"
printf "Group %5s = %d\n" "tcsh" "$gnr_tcsh"

你应该进行更多的错误检查。例如,测试您是否确实有一个名为 bash 的组。

然后,您可能需要一些变量来存储 GID,您希望将 GID 上的第一个 alpha 转换为 tail。然而,从您的任务描述来看,尚不清楚这是否应该在 bash/tcsh 组切换之前或之后完成。

无论如何。如果将 sed 包装在 bash 脚本中,则可以利用的一件事是通过以下方式使用 bash 变量暂时地转义 sed。此外,您可以像使用 awk 一样对 sed 命令进行分组,例如:

/pattern/ { exec if match}
/pattern/ ! { exec if no match }

这是一个显示我的意思的示例。但在这个具体示例中,它变得有点多余。我还添加了一些额外的输出,这在编写时可以很好地清晰快速地看到所做的事情:

gid_tr_to_uname=121

sed '
/:\/bin\/bash$/ {
    # Add an arrow only to visualize that line has changed
    s/^/--> /p
    # Susbtitute group
    s/\(^[^:]*:[^:]*:[^:]*:\)\([^:]*\)/\1'$gnr_bash'/
}
/:\/bin\/tcsh$/ {
    # Add an arrow only to visualize that line has changed
    s/^/--> /p
    # Susbtitute group
    s/\(^[^:]*:[^:]*:[^:]*:\)\([^:]*\)/\1'$gnr_tcsh'/
}
/[^:]*:[^:]*:[^:]*:'$gid_tr_to_uname':/ {
    # Insert line to visualize change [ old/new ]
    i\
tr group alpha name [
    p
    s/a\([^:]*:[^:]*:[^:]*:[^:]*\)\([0-9]\)\(:[^:]*:[^:]*:.*\)/a\11\3/
    s/b\([^:]*:[^:]*:[^:]*:[^:]*\)\([0-9]\)\(:[^:]*:[^:]*:.*\)/b\12\3/
    s/c\([^:]*:[^:]*:[^:]*:[^:]*\)\([0-9]\)\(:[^:]*:[^:]*:.*\)/c\13\3/
    s/d\([^:]*:[^:]*:[^:]*:[^:]*\)\([0-9]\)\(:[^:]*:[^:]*:.*\)/d\14\3/
    s/e\([^:]*:[^:]*:[^:]*:[^:]*\)\([0-9]\)\(:[^:]*:[^:]*:.*\)/e\15\3/
    s/f\([^:]*:[^:]*:[^:]*:[^:]*\)\([0-9]\)\(:[^:]*:[^:]*:.*\)/f\16\3/
    s/g\([^:]*:[^:]*:[^:]*:[^:]*\)\([0-9]\)\(:[^:]*:[^:]*:.*\)/g\17\3/
    s/h\([^:]*:[^:]*:[^:]*:[^:]*\)\([0-9]\)\(:[^:]*:[^:]*:.*\)/h\115\3/
    # ....
    # Append line to visualize end
    a\
]
}
' "$in_file"

阿尔法事物看起来不太好 - 下面的第二部分。

如果您可以使用 bash 而不是 sed,则可以通过将结果传送到 bash 循环来简化 alpha 转换,其中 IFS (如 FS 或 awk 中的字段分隔符)设置为:

#      capture group 1             capture group 2
#   s (everything before gid) gid (everything after gid) trigger / \1 new gnr \2
sed \
-e 's/\(^[^:]*:[^:]*:[^:]*:\)[^:]*\(.*:\/bin\/bash$\)/\1'$gnr_bash'\2/' \
-e 's/\(^[^:]*:[^:]*:[^:]*:\)[^:]*\(.*:\/bin\/tcsh$\)/\1'$gnr_tcsh'\2/' \
"$1" |
while IFS=: read account password uid gid gecos directory shell; do
    case "$gid" in
    "$gid_tr_to_uname")
        gid=$(translate "$account" "$gid")
    esac
    printf "%s:%s:%d:%d:%s:%s:%s\n"\
        "$account" "$password" "$uid" "$gid" "$gecos" "$directory" "$shell"
done

一些翻译功能如下:

ascii_a=$(printf "%d" "'a")
ascii_A=$(printf "%d" "'A")
translate()
{
    local first_letter="${1:0:1}"    # First character in arg 1
    local -i gid_lhs="${2:0: -1}"    # Everything but last digit in arg 2
                                     # Get ascii 10 base value / digit
    local -i ascii_val=$(printf "%d" "'$first_letter")
    local -i alphanr                 # a=1 b=2, A=27 etc
    if (( $ascii_val >= ascii_a )); then
        (( alphanr = ascii_val - ascii_a + 1 ))
    else
        (( alphanr = ascii_val - ascii_A + 27 ))
    fi
    # If you want to debug:
    # printf "[[[%s = %d => %d || %d ]]]"\
    #        "$first_letter" "$ascii_val" "$alphanr" "$gid_lhs"
    printf "%d%d" "$gid_lhs" "$alphanr"
}

但是,人们也可以轻松地添加一个case switchfor shell,那么 sed 就完全不适用了。

在 sed 中,你也有tr类似的功能y

sed '/0x[0-9a-zA-Z]*/ y/abcdef/ABCDEF' file

但它必须是偶数对,所以你不能将其用于 a -> 1, ... p-> 16 等。


第 2 部分:sed - 查找表

到目前为止,我能想到的将帐户首字母附加到 GID 的唯一方法是通过查找表。

为了简化,我分阶段进行:

清单 1

#!/bin/bash

listing1()
{
sed '
    # Pad line with lookup table
    s/$/0zero1one2two3three4four5five6six7seven8eight9nine/

    # Match something (here 1) and match it again in lookup-table
    # and grab the letters following 1 (in lookup-table) to match
    # group 2. Finally replace \1 with \2
    s/\(.\).*\1\([^0-9]*\).*/\2/
    #   |   | |     |     |   |
    #   |   | |     |     |   +----- Replace all with \2 which is "one".
    #   |   | |     |     +--------- Rest of line "2two3three4fo...".
    #   |   | |     +--------------- Match the word "one" and add it to
    #   |   | |                      group \2
    #   |   | +--------------------- Match group \1 => "1"
    #   |   |                        here in lookup-table : "1one"
    #   |   +----------------------- Match greedy => "23450zero"
    #   +--------------------------- Match one chr, that would be "1" from
    #                                input "12345\n", and add it to group \1

' < <(printf "12345\n" )
}

printf "Listing 1:\n"
listing1

结果:

Listing 1:
one

这个想法是用查找表填充我们的行,并将输入中的第一个匹配项替换为表中的相应对。

我们可以通过重复替换来扩展它:

清单 2

listing2()
{
sed '
    s/$/.0zero1one2two3three4four5five6six7seven8eight9nine/
    s/\([0-9]\)\(.*\)\1\([^0-9]*\)\(.*\)/\3\2\4/
    s/\([0-9]\)\(.*\)\1\([^0-9]*\)\(.*\)/\3\2\4/
    s/\([0-9]\)\(.*\)\1\([^0-9]*\)\(.*\)/\3\2\4/
    s/\([0-9]\)\(.*\)\1\([^0-9]*\)\(.*\)/\3\2\4/
    s/\([0-9]\)\(.*\)\1\([^0-9]*\)\(.*\)/\3\2\4/
    s/\([0-9]\)\(.*\)\1\([^0-9]*\)\(.*\)/\3\2\4/
' < <(printf "12345\n" )
}

结果:

Listing 2:
onetwothreefourfive.0zero6six7seven8eight9nine

但这看起来并没有比我们在第一节中开始的好多少。

标签/分支

这就是标签发挥作用的地方。sed可以指定标签,或者分支机构,并根据两个函数跳转到这些:

:my_label
     s/foo/bar/
     b my_label

简单b地说就是跳转到my_label。在这个例子中,这意味着一个永恒的循环。因此,大多数情况下,它的用法如下:

:my_label
/\./ {          # If . exists in line
     s/#/+/     # substitute # with +
     s/\./P/    # substitute . with P
     b my_label # goto my_label
}

这不是最好的例子,但希望你能明白。

第二种方法是使用 test 或t.这表示如果行发生更改,则转到标签。

:my_label
     s/foo/bar/   # Substitute foo with bar
     t my_label   # If there was a change aka; a substitution was done
                  # then goto my_label.

通过这个我们可以简化我们之前的清单如下。这里添加了逗号以使阅读更愉快:

清单 3

listing3()
{
sed '
    s/$/.0zero1one2two3three4four5five6six7seven8eight9nine/
:loop
    s/\([0-9]\)\(.*\)\1\([^0-9]*\)\(.*\)/\3,\2\4/
    t loop    # If we has a substitution goto loop

    s/,\..*// # Remove trailing comma and our lookup table rest.

' < <(printf "123458\n" )
}

结果:

Listing 3:
one,two,three,four,five,eight

我们想要阿尔法数字。另外,使用点作为分隔符可能会有一定的风险,因为我们的输入.中可能有一个 - 因此我们将其更改为使用 ASCII 0x7f 或 DEL。

它也适用于例如0x00

清单 4

listing4()
{
sed '
    p # Print original line to visualize

    # Our new lookup-table:
    s/$/\x7fa1b2c3d4e5f6g7h8i9j10k11l12m13n14o15p16q17r18s19t20u21v22w23x24y25z26/
:loop
    s/\([a-z]\)\(.*\)\1\([^a-z]*\)\(.*\)/\3,\2\4/
    t loop

    s/,\x7f.*//
' < <(printf "abcdefghijklmnopqrstuvwxyz\n" )
}

结果:

Listing 4:
abcdefghijklmnopqrstuvwxyz
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26

如果我们每个都有一个以上,我们会将其更改为如下所示:

清单 5

listing5()
{
sed '
    i\
input:
    p
    s/$/\x7fa1b2c3d4e5f6/
:loop
    s/\([a-z]\)\(.*\)\1\([^a-z]*\)\(.*\)/\3,\2\1\3\4/g
    t loop
    i\
output:
    s/,\x7f.*//
' < <(printf "aabcdefac\n" )
}

结果:

Listing 5:
input:
aabcdefac
output:
1,1,2,3,4,5,6,1,3

现在我们终于准备好在任务中实现它了。这里举个例子:

清单 6

listing6()
{
sed '
    i\
input:
    p
    s/$/\x7fa1b2c3d4e5f6g7h8i9j10k11l12m13n14o15p16q17r18s19t20u21v22w23x24y25z26/
    s/^\(.\)\([^:]*\)\(:[^:]*\)\(:[^:]*\)\(:[^:]*\)\([0-9]\)\(:.*\)\x7f.*\1\([^a-z]*\).*/\1\2\3\4\5\8\7/
    #  1alpha 2rest    3pwd      4uid      5gid    6last-digit 7rest           8number
    i\
output:
    s/,\x7f.*//
' < <(printf "master:power:110:118:Light Display Manager:/var/lib/lightdm:/bin/false\n" )
}

输出:

Listing 6:
input:
master:power:110:118:Light Display Manager:/var/lib/lightdm:/bin/false
output:
master:power:110:1113:Light Display Manager:/var/lib/lightdm:/bin/false

就是这样。


你应该阅读布鲁斯·巴尼特的sed简介.

其他参考:

对于一些更核心的东西,请看例如:


祝你好运。

相关内容