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="[email protected]"
# % 匹配@后面的字符并删除
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 [email protected]
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

如果你喜欢这篇文章,欢迎赞赏作者以示鼓励