我正在尝试学习 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 switch
for 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简介.
其他参考:
对于一些更核心的东西,请看例如:
- Greg Ubben 的 sed dc。与一个简短的解释。
- 塞德俄罗斯方块伴随着bash 包装器。
祝你好运。