Shell
这里记录我容易忘记的 shell 代码片段
30. 批量修改文件名
$ for f in prefix_*; do echo mv "$f" "${f#*_}"; done
去掉文件的前缀 prefix_, 实际替换时去掉上述命令中的 echo
29. 查看文件/进程被谁独占
btrfs 创建的snapshot会被其他用户或者终端占用,删不了很烦。使用lsof folder
查看被占用的进程,然后kill掉。
使用 lsof -i:8080
查看端口被占用的进程。
28. mutt 发邮件
#!/bin/bash
echo "Subjuct: "$SUBJECT
To=$SEND_TO
MSG=`echo -ne "message use mut \nThanks & Regards,\nzdd\n"`
# sudo apt-get install mutt
echo "$MSG" | mutt -s "$SUBJECT" -c $CC $To
27. 冒号 : 的作用
- 空语句或者注释
: 如果下面的 if 语句没有 : 将报语法错误
: I am also a comment
if [ -z "" ]; then
:
fi
# 注意,当遇上重定向时,: 注释失效
: >file # 将清空 file 文件的内容
- 字符串的默认参数
echo ${VAR:=hello} # 如果 $VAR 没有值,则赋值成 hello echo ${VAR:+hello} echo ${VAR:?hello}
26. 进程替代 (Process Substitution)
Linux 管道(前一个命令的输出做为后一个命令的输入)的应用非常普遍。有时候,我们需要将多个命令的输出做为某一个命令的输入,或者一个命令的输出重定向到多个命令的输入。进程替代就是为了完成这种需求出现的。语法:
>(command_list)
<(command_list)
# 注: 括号和>/<号之间没有空格
# 例如:
diff <(ls $first_directory) <(ls $second_directory)
# 可将 <(command_list) 或 >(command_list) 看成运行 命令 command_list 后将结果保存成名为 /dev/fd/<n> 的文件, 例如
echo a >(True) <(True) # 和运行 echo a b c 的效果相同,只是文件名不同
25. Shell 阶段小结
最近一段时间,发现写 shell 脚本的次数比较多,以至于不 google, 不查文档也能写出可用的 shell 代码片段,在这里做一个阶段性的总结。
- 常用操作
#!/bin/bash
# $(...)等价于``, 以下两行等价
A=`echo "Hello zddhub"`
B=$(echo "Hello zddhub")
# 用[] && 或 [] || 取代 if 语句,这不一定是推荐的做法,但比较简练
[ "$A" == "$B" ] && echo "A == B" # 判断后执行单语句时推荐
# 获取管道中各段命令的返回值
[ "$A" == "$B" ] | echo "A == B" | grep C
echo ${PIPESTATUS[*]}
[ "$A" == "$B" ] | echo "A == B" | grep C
echo ${PIPESTATUS[0]} ${PIPESTATUS[1]} ${PIPESTATUS[2]}
# 用引号和不用引号的区别
C=`ps`
echo $C # 压缩成一行
# PID TTY TIME CMD 24846 pts/11 00:00:00 bash 25058 pts/11 00:00:00 ps
echo "$C"
# PID TTY TIME CMD
# 24846 pts/11 00:00:00 bash
# 25058 pts/11 00:00:00 ps
# 命令中的函数需要 export, 如:
function print_info() {
echo $REPO_PROJECT $REPO_PATH
}
export -f print_info
repo forall -c 'bash -c print_info' # export 后, print_info 才能被识别
# 字符串截取
email="zddhub@gmail.com"
# % 匹配@后面的字符并删除
echo ${email%@*} # zddhub
# # 匹配@前面的字符并删除
echo ${#email#*@} # gmail.com
str="a b c d"
d="d"
[ "${str/"$d"}" != "${str}" ] && echo "[str] include $d"
# 简单缩进
function indent() { sed 's/^/ /'; }
echo "hello"
echo "zdd" | indent
echo "zddhub" | indent | indent
# 简单缩进中的坑
function blog() {
blog="www.zddhub.com" # 默认为global变量,作用域从定义到文件结尾,或者显示删除变量行后
echo "print $blog"
}
blog | indent
echo "global variable: " $blog # 值为空
# 这是由于 subshell 引起的, 即 bash 会创建子进程运行 `pipe/\`\`/$()` 中的命令。当进程结束时变量丢失。
# 目前没有解决方案,如果你的函数是函数式的 (当输入相同时,多次调用输出也相同), 可用以下 workaround 的方法:
blog > /dev/null # discard output
echo $blog # get blog value
blog | indent # run again to indent
# 如果你有更好的方法,请联系我!
# 然而如果想得到 indent 函数里的变量,却可以用进程替代 (Process Substitution) 实现:
function indent() {
sed 's/^/ /'
subs="I am in indent can can go out!"
}
indent < <(blog) # work
echo "global variable" $subs
-
重定向 0
Linux 将所有设备都抽象成文件,在 Linux 系统中用文件描述符区分,其中最特别的就是:
- 标准输入 0
- 标准输出 1
- 标准错误输出 2
在每条 shell 命令执行时都被会默认打开,默认的设备都是终端。
通常在保存 log 时会把标准错误输出和标准输出保存到同一个文件,使用 2>&1 实现。
-
为什么不用2>1?
shell 在分析 2>1 时会把 1 理解为文件名为 1 的文件,所以加 & 区分。
-
解释以下两个命令的区别:
$ ls zddhub 2>&1 >a ls: cannot access zddhub: No such file or directory $ ls zddhub >a 2>&1 $
shell 实现重定向时使用系统调用 dup2(a,b): dup2 会创建一个新的文件描述符 b,内容 copy 自 a。如果 b 存在,则先关闭后创建。如果 a, b 相等,则直接返回。
执行
ls zddhub 2>&1 >a
时系统的调用顺序为:dup2(1, 2) 3=open(a) dup2(3, 1)
文件描述符内容的变化:
stdin - 0 stdout - 1 stderr - 2 a - 3 dup2(1,2) ternimal stdout stdout a dup2(3,1) ternimal a stdout a 此时,shell 程序写到标准输出的信息会保存到文件 a,但时写到标准错误输出的信息依然从 stdout (终端)输出。
执行
ls zddhub >a 2>&1
时系统的调用顺序为:3=open(a) dup2(3, 1) dup2(1, 2)
文件描述符内容的变化:
stdin - 0 stdout - 1 stderr - 2 a - 3 dup2(3,1) ternimal a stderr a dup2(1,2) ternimal a a a 这种情况下, stdout/stderr 对应的设备都指向了文件 a.
这种基于 dup2 的后台实现和 > (水流)方向正好相反,极容易混淆又比较常用,bash 4+ 提供了新的写法:
ls zddhub &>a
将标准输入输出和错误输入输出重定向到文件 a。用 tee 同样能达到效果:
ls zddhub 2>&1 | tee a
-
多条命令使用相同的重定向
exec 3<&1 # stdout 重定向到文件描述符 3 exec 3>&- # 取消文件描述符 3 的重定向
-
重定向 1
如果当前的命令会影响其执行环境中的标准输入输出或者错误输出时,需要提前将循环时的输入输出重定向到新的文件描述符, 以防受其影响。
#!/bin/bash # 将命令结果保存到文件,再循环处理 echo "$(ps)" > a while read line; do echo "$line" done < a rm a # 直接从变量中处理, 不用保存文件 while read line; do echo "$line" done <<< "$(ps)" # 如果循环体中的命令会影响标准输入输出/标准错误输入输出时,需要重定向到新的文件描述符,防止混乱。 while read -u 3 line; do echo "$line" read -p "Press enter to continue" done 3<<< "$(ps)" # 从文件中读 echo "$(ps)" > a exec 4<a while read -u 4 line; do read -r -p "Print ? [Y/n]: " response case $response in [Nn]|[Nn][Oo]) continue; esac echo "$line" done exec 4>&- rm a # 同时从两个文件读 exec 3<$1 exec 4<$2 while read line1 <&3 && read line2 <&4 do echo "$line1" echo "$line2" done
24. ssh 远程登录太慢
很不幸,遇到了 ssh hang 住的 bug,简直不能忍:
OpenSSH_5.9p1 Debian-5ubuntu1.4, OpenSSL 1.0.1 14 Mar 2012
debug1: Reading configuration data /home/zddhub/.ssh/config
debug1: Reading configuration data /etc/ssh/ssh_config
debug1: /etc/ssh/ssh_config line 19: Applying options for *
debug2: ssh_connect: needpriv 0
<hang ~30s in this line>
查了很久,应该是 glibc 库的 bug,这里有详细的描述:
https://bugs.launchpad.net/ubuntu/+source/openssh/+bug/883201 (和#2的问题一模一样) https://bugs.launchpad.net/ubuntu/+source/eglibc/+bug/417757
奇怪的是相同版本号的 ubuntu 却没有类似问题,对比了 DNS 相关的配置文件,没有找到差异.
更改 hostname
N 天后,问题消失。怀疑是内网 hostname 冲突所致。
23. ssh 添加了public key 依然提示需要密码
被这个问题困扰了好久,网上的提示是.ssh目录文件权限不对,确实是这样的。
我遇到的坑是,不小心把文件的owner改成了700
。是的,在我的潜意识里,700
就是 -rwx------
,所以不管我怎么核对file mode 总是出错。折腾了很久才发现问题。
遇到这种问题,可查看log文件/var/log/auth.log
里查看出错信息。如下:
sshd[6087]: Authentication refused: bad ownership or modes for file ~/.ssh/authorized_keys
ownership
or modes 非常清楚,我就是找不到。
22. 从manifest.xml文件中获取path和revision
注意sed用法和字符串截取
#!/bin/bash
diff=`cat $1 | grep -o "path=.* revision=.*" | sed -e 's/.*path="\(.*\)" revision="\(.*\)".*/\1:\2/g'`
reset_using_sha1() {
# $1 - path
# $2 - sha1
echo ">>> path: "$1 "revision: "$2
cd $1 > /dev/null
if [ $? -ne 0 ]; then
echo ">>> fix git: please check into $1 $2"
return
fi
git log | grep -m 1 $2 1>/dev/null
if [ $? -eq 0 ];then
git reset --hard $2
if [ $? -ne 0 ];then
echo ">>> fix git: please check into $1 $2"
fi
else
echo ">>> fix git: please check into $1 $2"
fi
cd - > /dev/null
}
for entry in $diff;do
path=${entry%%:*}
sha1=${entry#*:}
reset_using_sha1 $path $sha1
done
21. 根据topic名review and verified
#!/bin/bash
GERRIT_URL="gerrit.com"
gerrit_info=`ssh -p 29418 $GERRIT_URL gerrit query --current-patch-set status:open topic:topic_name`
ref=`echo "$gerrit_info" | sed -n 's:ref\:::p' | sed 's/^[ \t ]*//g'`
# echo $ref
for i in `echo "$ref"`;do
#echo $i
change=`echo "$i"| cut -d '/' -f 4`
patchset=`echo "$i"| cut -d '/' -f 5`
#echo "ssh -p 29418 $GERRIT_URL gerrit review --code-review +2 --verified +1 $change,$patchset"
ssh -p 29418 $GERRIT_URL gerrit review --code-review +2 --verified +1 $change,$patchset
done
20. 获取gerrit上的cherry-pick 链接
#!/bin/bash
GERRIT_URL="gerrit.com"
gerrit_info=`ssh -p 29418 $GERRIT_URL gerrit query --current-patch-set change:$1`
project=`echo "$gerrit_info" | sed -n 's:project\:::p' | sed 's/^[ \t ]*//g'`
ref=`echo "$gerrit_info" | sed -n 's:ref\:::p' | sed 's/^[ \t ]*//g'`
echo "git fetch ssh://$GERRIT_URL:29418/$project $ref && git cherry-pick FETCH_HEAD"
19. 批量替换
$ sed -i "s/oldstring/newstring/g" `grep -irl "oldstring" *`
18. 如果遇到基础问题
# 如果是基础问题,一定要自己搞定 - 一个同事的名言,感谢。
17. MacOS sed issue
When run sed command on MaxOS, error will emit:
localhost:duck zdd$ sed -i "s/old/new/g" a
sed: 1: "a": command a expects \ followed by text
是因为MacOS下的sed命令需要一个backup文件的参数,用来保存你原始文件,避免错误的修改之后无法恢复。只要指定这个文件名就好了。
$ sed -i .bk "s/old/new/g" a # 保存原始文件为a.bk
$ sed -i "" "s/old/new/g" a # 不保存备份
16. 设置最大可打开的文件数
$ ulimit -n 1024 # mac
15. shell 回显调试
比起bash -v, -x 更适合调试了
#!/bin/sh -x
# 在变量替换之后、执行命令之前,显示脚本的每一行
14. 添加文件到另一文件的指定行
$ sed -i '2 r a.txt' b.txt
# 插入a.txt的内容到b.txt的第二行
13. aptitude
相比apt-get, aptitude会自动处理依赖
aptitude update
更新可用的包列表
aptitude upgrade
升级可用的包
aptitude dist-upgrade
将系统升级到新的发行版
aptitude install pkgname
安装包
aptitude remove pkgname
删除包
aptitude purge pkgname
删除包及其配置文件
aptitude search string
搜索包
aptitude show pkgname
显示包的详细信息
aptitude clean
删除下载的包文件
aptitude autoclean
仅删除过期的包文件
12. apt-get install
当在ubuntu上安装某些包时,会提示: When use apt-get install, if show depends … 只使用
$ apt-get –f install
不要加包名在后面
11. shell快捷键
Shortcut Description
CTRL-A Move cursor to beginning of line
CTRL-E Move cursor to end of line
CTRL-R Search bash history
CTRL-W Cut the last word
CTRL-U Cut everything before the cursor
CTRL-K Cut everything after the cursor
CTRL-Y Paste the last thing to be cut
CTRL-_ Undo
10. 系统信息查询
# ps: 用了这么久的linux,居然没看过系统信息,也是醉了。
$ cat /proc/meminfo
$ cat /proc/cpuinfo
$ /proc/里有一切...
$ df -h
$ lscpu lsusb lshw
$ ...ls可以看一切
# 话说,我每次遇到命令的用法都是谷歌的,以后请优先使用man
$ man man
9. 大小写转换
$ LowChar=`echo $line | tr 'A-Z' 'a-z'`
$ UpChar=`echo $line | tr 'a-z' 'A-Z'`
8. 删除匹配行
$ sed -i '/#$/d' filename
7. find 常用命令
# 查找并删除
$ find . –name “*.xml” –exec rm {} \;
6. 远程登陆简化脚本
#!/usr/bin/expect
set timeout 30
spawn ssh zdd@github.com
expect "*assword:"
send "password\r"
interact
5. Shell 读取指定列
$ll | grep xxx | awk ‘{print $9}’
# 或
$ll | grep xxx | cut –c 55-
4. Shell 读两个文件
#!/bin/sh
exec 3<$1
exec 4<$2
while read line1 <&3 && read line2 <&4
do
echo "$line1"
echo "$line2"
done
3. Shell Usage提示
if [ $# -lt 2 ]; then
echo "Usage: xx.sh argv1 argv2"
echo "\tmake sure xxx"
exit 1
fi
2. Shell 多行输出
cat << INFO
some info
some info
INFO
1. Install deb
sudo dpkg -i *.deb
如果你喜欢这篇文章,欢迎赞赏作者以示鼓励