bash及zsh语法

执行命令或文件

以下对zsh与bash同时成立

参考

下述文件为shell语言的脚本,常见为.sh文件或.bash_xxxx类文件

在当前shell执行

  • 使用当前shell的变量/函数/当前路径, , 不论是否export
  • 创、变、删的 变量/函数/当前路, 不论是否export, 都会保存在当前shell里面

source 文件 (等价于 . 文件

  • 文件不必有可执行权限

命令 , { 命令块;} , 循环体/条件体

  • {后要有空格, }前要有;(但无需空格), 才能在bash运行, 不然只能zsh下运行
  • { 命令块;} , 循环体/条件体, 即相当于运行了匿名函数
  • { 命令块;}命令块产生相同的标准输出和标准错误

开新shell执行

./文件 <某种SHELL> 文件

  • 开新shell运行, 会加载执行的shell 的 profile 文件 (例如.profile, .zshenv)

  • 继承全局变量(即export了的变量):

    • 新shell继承 (即copy) 当前shell中 export 了的变量, 不继承当前shell中未export的变量
    • 新shell新建/改变 的 变量/函数/当前路径, 不论新shell是否export它们, 在新shell结束后注销, 不会被带回当前shell
    • 新shell结束后, 当前shell的变量均无变化
  • 执行的shell:

    • <某种SHELL> 文件, 包括sh 文件 , 文件不必有可执行权限
    • ./文件 等价于sh 文件,但要求文件有可执行(x)权限,授权方式chmod +x 文件
  • shell的区别

    • sh: 全称 POSIX shell, ./文件 所使用的shell, 所有 unix 系统共有
      • POSIX: 可移植操作系统接口Portable Operating System Interface of UNIX
    • bash: 全称 Bourne Shell, 增强版的sh, 更常用, 大多数Linux发行版默认的交互shell
    • zsh: 全称 z shell, 类似bash的shell, 改善交互体验
  • $SHELL 文件:用当前默认的交互shell(可能是zsh或bash等)执行,文件不必有可执行权限

    • $SHELL 是当前默认的交互shell
    • $SHELL -c '命令':用当当前默认的交互shell执行
    • chsh -s $(which <shell名>) : 修改默认的交互shell

开子shell执行

  • 不是另启一个新shell,故不会加载执行的shell 的 profile 文件 (例如.profile, .zshenv)
  • 开子shell, 与当前shell是同一个shell类型 (即, 是sh,bash, 还是zsh)
  • 继承父shell的所有变量, 不论是否export
  • 子shell新建的、改变的 变量/函数/当前路径 不会被带回父shell,不论是否使用export, 在子shell结束后注销, 子shell结束后父shell的变量均无变化

value=`命令`,等价于 value=$(命令)

  • 输出返回到value,value为一个字符串,可以有换行符;value不是数组

(命令块)

  • 命令块产生相同的标准输出和标准错误
  • ( 后与)前无需空格, 有也能运行; 命令块 后无需;, 有也能运行.

例子

pwd
(cd ..; pwd)
pwd

返回

/home/you/ENV/CONF
/home/you/ENV
/home/you/ENV/CONF

然而

pwd
cd ..; pwd
pwd

返回

/home/haoyu/ENV/CONF
/home/haoyu/ENV
/home/haoyu/ENV

管道 命令1 | 命令2

  • 命令1 (非管道结尾的命令), 均是在当前shell分别开子shell, 命令1 的修改/创建的变量(不论是否export), 不会留给 命令2 与 后续的父shell
a=1;   a=2 | echo $a ;   echo $a
a=1;   export a=2 | echo $a ;   echo $a
# bash 和 zsh 下均输出
# 1
# 1
  • 命令2 (管道结尾的命令) 在zsh下是 在当前shell执行, 改/创建的变量(不论是否export), 会留给后续的父shell
  • 命令2 (管道结尾的命令) 在bash下是 开子shell执行, 改/创建的变量(不论是否export), 不会留给后续的父shell
 a=1 b=1 c=1;  a=2 | b=2 | c=2; echo $a $b $c
# zsh下输出 1 1 2
# bash下输出 1 1 1

管道与循环体/管道组合

  • 命令|循环体, 在bash中循环体是子shell执行; 在zsh中循环体是当前shell执行

  • 循环体与重定向 ( <<< << < < <(...) )组合起来, 则循环体在bash和shell中均是用当前shell执行

  • 故而, 如需修改此代码块外的变量 (例如给一个array加成员, 给计数器加值), 建议能用重定向, 就用重定向写, 以便zsh和bash兼容.

    即, 要写成

    循环或条件体 <<< "${string}"
    循环或条件体 < <( 命令块 )

    而不要

    echo -E "${string}" | 循环或条件体
    ( 命令块 ) | 循环或条件体
    { 命令块; } | 循环或条件

    例如

    array=();
    while IFS= read -r line; do array+=("${line}"); done <<< \
    'asda'$'\n''adasadasd'$'\n''asdasdasdassd'
    declare -p array
    # bash 输出
    # declare -a array='([0]="asda" [1]="adasadasd" [2]="asdasdasdassd")'
    # zsh 输出
    # typeset -a array=( asda adasadasd asdasdasdassd )
    array=();
    echo -E 'asda'$'\n''adasadasd'$'\n''asdasdasdassd' | \
    while IFS= read -r line; do array+=("${line}"); done
    declare -p array
    # bash 输出
    # declare -a array='()'
    # zsh 输出
    # typeset -a array=( asda adasadasd asdasdasdassd )

代码块的性质

代码块的组合命令

不论哪种代码, 均有以下性质

  • 帽 代码块 靴 | 命令 : 表示对整个代码块的输出 经过管道 送给命令
  • 帽 代码块 靴 >文件 : 表示对整个代码块的输出 写入到文件. >>同理

循环体

  • for/while xxx do; 代码块; done 管道或写入文件 是对所有轮的输出 整体执行一次.
  • for/while xxx do; { 代码块; } 管道或写入文件; done 是每轮循环输出 执行一次管道/文件写入

例如

for i in {1..3}; do
    echo $i
done | sort -r # 降序

# 结果为
# 3
# 2
# 1

条件体

if ... if 管道或写入文件 不论对于哪个条件分支, 或不满足任何分支条件的情况, 均会执行

例如

rm result
if [ 1 -ne 1 ]; then
    echo '1 -ne 1'
    echo '1 != 1'
fi > result
# 结果为 创建了一个空的 result 文件

rm result
if [ 1 -ne 1 ]; then
{
    echo '1 -ne 1'
    echo '1 != 1'
} > result
fi
# 结果为 没有创建result文件

获得命令输出

$(...) `....`输出返回为匿名字符串

以下对zsh与bash同时成立

子shell执行

  • `xx` 完全等价于 $(xx):用当前shell开子shell执行xx命令,将其返回为无名变量。例如
echo `ls`
echo $(ls)
result=`ls`
result=$(ls)
  • `bash -c "xx"` 完全等价于 $(bash -c "xx"):用bash执行xx命令,将其返回为无名变量

输出

`xx` 完全等价于 $(xx)只获得xx命令的正常输出(>&1),不获得

  • xx的报错报警输出(>&2)仍然打印在屏幕
  • 若想xx重定向到文件的输出, 则写>文件

例如:

f()
{
  echo 'file' > /dev/null # 报错报警
    echo 'warring' >&2 # 报错报警
    echo 'normal' >&1 # 正常输报警
    echo 'normal' # 默认是正常输报警
}
return_string=$(f)
# 输出如下

warring

echo $return_string
# 输出如下

normal
normal

<(...)输出返回为匿名文件

以下对zsh与bash同时成立

<(命令), 将命令的输出返回为匿名文件.

注意:

  • 当前shell开子shell执行xx命令
  • 此文件只获得xx命令的正常输出(>&1), 报错报警输出(>&2)仍然打印在屏幕

辨析:

  • 命令 < <(echo -E "${string}”) 等价于 命令 <<< "${string}"

例如:

  • 比较两个目录的差异

    diff <(ls dirA) <(ls dirB)
  • 逐行读取多行字符串

    while IFS= read -r line; do
        echo "$line"
    done < <(echo -E "${string}")

    等价于

    while IFS= read -r line; do
        echo "$line"
    done <<< "${string}"

各种引号

以下对zsh和bash皆成立

转义

创建字符串, 各种引号

赋值给字符串时:

  • 单引号:所有都不转义

  • $'xxx': 仅\某转义

  • 双引号, <<EOF, <<-EOF${xxx}(变量)、`xx` $()(命令返回) 转义, 用 \$ \`xxx\`取消转义; \某 不转义

输出

从被输出的字符串, 到输出的字符串, 这个过程中:

  • echo -E 字符串: 输出 \某 不转义

  • echo [-e] 字符串: 输出 \某 转义, -e即默认选项 可省去

双引号的优先级

  • ”$(..."...${variable}..."....)””`..."...${variable}..."....`” 均表示: 含有将variable变量的字符串"...${variable}…” , 用于某个子shell命令..."...${variable}..."...., 其结果返回为一个字符串. 最外围的双引号不会跨过 $(…)`…` 的边界与其内部的双引号成对解析. 例如

  •   echo "$([ "${USER}" = "${USER}" ] && echo yes)"
      # 返回 yes
      
    
    *   `”$((..."...${variable}..."....))”`  会被解析成 `”$((...”` 字符串, `....` , `${variable}` 变量, `...`, ` "....))”` 字符串的concat. 例如
    
        ```bash
        a=1; b=2
        echo "$(("$a" + "$b"))"
        # 返回
        # zsh: bad math expression: illegal character: "

局部变量

  • 仅函数内可见的变量,只需首次用时写local,之后不用再写了。但不可用于非函数内 (如脚本中 不在函数里之处), 例如
f() {
    local variable=xxx
    variable=yyy # 再次使用,无需声明local

    local i=
    # 不可写作 local i; 不然若之前已经创建了local i, 则此处会输出 "i=xxxx"
    for i in "$@"; do
    # 不可写作 for local i in "$@"; do
        # xxxx
    done
}

f
echo ${varible} # 返回为空,因为在全局变量中,没有名为variable的变量
  • local variable= : local variable赋值为’’, 用于
  • local variable : 若无local variable, 则创建此variable; 若有local variable, 则显示"vairable=xxxx".
  • bash/zsh/sh 文件.sh及其./文件.sh运行脚本,脚本中所创、改、删的变量/函数不会带到运行它环境中(除非export),无需手动释放创建的变量/函数
  • source 文件.sh(即. 文件.sh),脚本中所创、改、删的变量/函数会带到运行它环境中(不论是否export),故需手动释放创建的变量/函数
unset -f 函数名
unset -v 变量名 # -v 可缺省

判断

以下适用于bashzsh

概述

if ( 判断语句 ); then
    do_something
fi

if [ 判断语句 ]; then
    do_something
fi

if [[ 判断语句 ]]; then
    do_something
fi

                              # 否定条件则写作:
( 判断语句 ) && do_something   # ! ( 判断语句 )  && do_something   也即   ( ! 判断语句 )  && do_something
也即 判断语句 && do_something   # 也即 ! 判断语句 && do_something

[ 判断语句 ] && do_something   # ! [ 判断语句 ]  && do_something

[[ 判断语句] ] && do_something # ! [[ 判断语句 ]]  && do_something

判断语句

参考

( 判断语句 ) 也即 判断语句 含义 示例
command -v 命令 > /dev/null 判断命令知否存在(命令可以使alias以及自定义的shell函数) if command -v pwd > /dev/null; then 也即 if ( command -v pwd > /dev/null ); then
注:[ command 命令 2>&1 | grep 'command not found'] 辨析:判断命令知否存在(命令为系统原生命令,不包含可以使alias以及自定义的shell函数) [ command ls 2>&1 | grep 'command not found']
[ $? -eq 0 ] $?表示前面所有代码里是报错的编码是啥, 为0<=>无错误 [ $? -ne 0 ] && echo Error
[ 判断语句 ] (也可用[[ ]]) 含义 示例
文件比较
-e file 存在文件或文件夹,或存在一个有效(最终(可以符号逻辑递归)指到文件或文件夹)的符号链接 [ -e /var/log/syslog ]
-d file 存在且为目录 [ -d /tmp/mydir ]
-f file 存在且为常规文件 [ -f /usr/bin/grep ]
-L file 为符号链接,不论是否有效 [ -L /usr/bin/grep ]
-r file 存在且当前用户可读 [ -r /var/log/syslog ]
-w file 存在且当前用户可写 [ -w /var/mytmp.txt ]
-x file 存在且当前用户可执行 [ -L /usr/bin/grep ]
-c file 存在且为字符特殊文件
-b file 存在且为块特殊文件
-s file 存在且文件大小非0,即文件存在且非空
-t file 文件描述符(默认为1)指定的设备为终端 [ -t 1 ] 表示当前命令会显示在屏幕,即当前处在交互终端下
file1 -nt file2 file1 比 file2 新 [ /tmp/install/etc/services -nt /etc/services ]
file1 -ot file2 file1 比 file2 旧 [ /boot/bzImage -ot arch/i386/boot/bzImage ]
字符串比较 要用引号
-z string 长度为零,即串空 [ -z “$myvar” ]
-n string 长度非零,即串非空 [ -n “$myvar” ]
-z ${var+x} 变量var是否有定义 [ -z “${myvar+x}" ]
string 与上等价 [ “$myvar” ]
string1 = string2 string1 与 string2 相同 [ “$myvar” = “one two three” ]
string1 != string2 string1 与 string2 不同 [ “$myvar” != “one two three” ]
布尔值
"$mybool" = true 是否真 [ "$mybool" = true ]
"$mybool" = false 是否假 [ "$mybool" = false ]
"$myboo1l" = "$mybool2" 是否同 [ "$myboo1l" = "$mybool2" ]
"$myboo1l" != "$mybool2" 是否异 "$myboo1l" != "$mybool2"
[[ 判断语句 ]] 含义 示例
子串
"$string" =~ "$substring” "$string" =~ 'xxxx' 是否string包含substring (bash中时普通字符串, zsh中是正则表达式),例如 [[ "$string" =~ "$substring" ]][[ "$string" =~ "balabala" ]]
"$string" =~ $substring 是否string包含substring (zsh和bash中皆是正则表达式),例如 [[ "$string" =~ ^[0-9]+$ ]]
算术比较
num1 -eq num2 == [[ 3 -eq $mynum ]]
num1 -ne num2 != [[ 3 -ne $mynum ]]
num1 -lt num2 < [[ 3 -lt $mynum ]]
num1 -le num2 <= [[ 3 -le $mynum ]]
num1 -gt num2 > [[ 3 -gt $mynum ]]
num1 -ge >= [[ 3 -ge $mynum ]]

注意

  • 运算符前后要有空格至少一个
  • [、[[]、]]前要有空格至少一个

其他特殊判断

式子 含义 说明
[ "${variable-no}" != no ] 变量variable有定义 若变量 variable无定义, 则${variable-xxxx}返回“xxxx”
variable有定义, 则返回${variable}
variable= : 等价于 variable=‘’ , 故有定义
unset variable : 注销variable, 故无定义

逻辑运算

以下以[ 判断语句 ]为例,[[ 判断语句 ]]同理

逻辑运算 含义
[ 判断语句 ] && [ 判断语句 ] 与,次次优先
[ 判断语句 ] || [ 判断语句 ] 或,次次优先
! [ 判断语句 ] [ ! 判断语句 ] 非,次优先 ! 和 [ 中间必需有至少一个空格
( 上述三种运算 ) 结合,最优先 if ! ( [ 判断1 ] || [ 判断2 ] ) && [ 判断3 ]; then dosomething; fi

[[ ... ]][ ... ] 辨析

  • linux的sh不支持[[...]]
  • 在zsh,bash,mac的sh, linux的sh 中:
    • 凡是能用[...]的判断语句, 都能用 [[...]]
    • [[...]] 可以把 &&, || , (), ! 写在里面: 例如[[ 1 = 1 && 2 =2 ]], [[ ( ! 判断1 || 判断2 ) && ( 判断3 )]]
  • 在linux与mac的sh/bash/zsh中, […], [[...]] . 它们都可以把 && ,||, (), ! 写在里面: 例如 [ ! 1 = 2 ] [[ ! 1 = 2 ]]

输入

get one char

answer=$(bash -c "read  -n 1 -p '<要显示的提示句> ? [Y|N]' c; echo \$c"); echo
# 调用bash执行read -n 1 -p '<要显示的提示句> c', 即将提示句显示,等待输入字符,抓取即赋值给c
# 将c返回到answer
# echo:换行
  • 适用于:zsh、bash、sh、./文件运行可执行文件、任何当前终端中
  • 功能:等待输入,一旦输入一个字符(回车也算一个字符),立即结束等待,将这个字符返回给answer变量

注:

  • 请不要使用

    echo -n '<要显示的提示句> ? [Y|N]'
    answer=$(bash -c "read  -n 1 c; echo \$c"); echo

    因为使用sh、./文件运行可执行文件 的方法运行上述命令,echo不支持-n参数,会输出"-n"

询问Y/N

while true; do
    # echo - '提示?[Y/n]'
    answer=$(bash -c "read  -n 1 -p '<要显示的提示句>? [Y/N] ' c; echo \$c"); echo
    if [ "$answer" = "y" ] || [ "$answer" = "Y" ]; then
        # 回答是则如何操作
        break
    elif [ "$answer" = "n" ] || [ "$answer" = "N" ]; then
        # 回答否则如何操作
        break
    else
        echo 'input Y/y or N/n please!'
    fi
done

例如

<提示句>? [Y|N]n
# 回答否的操作

重定向

输入重定向

<<<<

  • 命令 < 文件 等价于: 执行 命令 然后在 stdin 输入 { cat 文件 之所得}
  • 命令 <<< 字符串 等价于: 执行 命令 然后在 stdin 输入 字符串

例如:

cat <<< "${HOME}"
/home/haoyu/ENV/CONF
cat <<< '${HOME}'
$HOEM
sed 's/a/b/g' <<< "aaa"
bbb

cat < 文件 等价于 cat 文件

echo 'aaa' > file
sed 's/a/b/g' <<< file
bbb

读取文件到变量

  • 不保留结尾空白字符和开头空白字符

    read -r -d '' 变量 < 文件
    • -r: raw string, 即原始的字符串
    • -d 分隔字符: 以分隔符将分割得到的字符串数组给变量. 当其为 '' 即不要分割.
  • 保留开头空白字符, 但不保结尾空白字符

变量=$(<文件)
# 等价于
变量=$(cat 文件)
  • 保留开头/结尾空白字符

    file_to_var() {
      local file_name="$2"
      local var_name="$1"
      local file_to_var_var=''
      while IFS= read -r line ; do
        file_to_var_var+="$line"$'\n';
      done < $file_name
      file_to_var_var="${file_to_var_var:0:-1}"
      eval "$var_name"'="${file_to_var_var}"'
    }
    file_to_var 文件 变量

<< 多行字符串

  • 总述

    命令 <<[-]EOF
      多行内容1
      多行内容2
    EOF
    • 以上等价于: 执行 命令 然后将 <<[-]EOFEOF 之间的字符串 输入到 stdin
  • << / <<-EOF 间可以有空格

    • EOF 可以改成任意 [a-zA-Z_]+ 的字符串
  • <<- 表示去掉多行内容再每行开头的所有 tab, << 则不去掉

    • 结尾的 EOF 必需在行首, 无缩进
    • 不转义\x, 仅转义 ${xxx} $xxx $(xxx) `xxx`, 取消转义需 $ 写做 \$, ` 写做 \` .
  • 输出多行字符串:

    cat <<EOF
      多行内容1
      多行内容2
    EOF

    输出

    多行内容1
    多行内容2
  • 取消tab缩进, 但空格缩进不会取消:

cat <<-EOF
  多行内容1 # 开头是 tab, 不是空格
  多行内容2 # 开头是 tab, 不是空格
EOF

输出

多行内容1
多行内容2
  • 多行字符串写到文件

  • 法一: > 无法以 sudo 形式写入到文件

  cat > 文件 << EOF
    多行内容1
    多行内容2
EOF
  • 法二 [更灵活, 推荐之] : 允许以 sudo 形式写入到文件
cat << EOF | [ sudo ] tee [-a] 文件 [ >/dev/null ]
  多行内容1
  多行内容2
EOF
  • 多行字符串赋值给变量

  • 不保留结尾空白字符和开头空白字符

read -r -d '' var << EOF
  多行内容1
  多行内容2
EOF

# 或将命令块的返回作为多行字符串返回给var
read -r -d '' var < <(命令块)
  • 保留开头/结尾空白字符
var=''; while IFS= read -r line ; do var+="$line"$'\n'; done << EOF && var="${var:0:-1}"
  多行内容1
    多行内容2
EOF

输出重定向

>>>

写文件的模式

  • >: write模式:即若有文件,则重写文件;若无文件,则新创而写之
  • >>: append模式:即若有文件,则追加到文件尾;若无文件,则新创而写之

标准输出、标准错误重定向到文件

  • 命令 >文件名=命令 1>文件名 :重定向标准输出到文件,write模式
  • 命令 2>文件名:重定向标准错误到文件,write模式
  • 上述 >改为 >>,即变成append模式
  • >or >>) 与文件名之间可以有空格, 但与&1 &2 之间不能有空格

常用文件名

  • /dev/null:可无限写入、但不保存内容的“黑洞“文件
  • &1:标准输出,会显示在显示屏
  • &2:标准错误,也会显示在显示屏

标准输出、标准错误相互重定向

  • 2>&1:将标准错误重定向到标准输出——标准错误不再有原标准错误的内容,而标准输出同时有远标准错误、标准输出的内容。
  • 1&>2:将标准输出重定向到标准错误,效果同上理

重定向的组合命令:重定向从后往前执行,如 命令 >文件名 2&>1 先执行 2&>1 后执行 >文件名

  • 命令 &>文件名 = 命令 >文件名 2&>1 (≠ 命令 2&>1 >文件名):标准输入和输出都重定向到文件,write模式
  • 命令 &>>文件名= 命令 >>文件名 2&>1 (≠ 命令 2&>1 >>文件名):标准输入和输出都重定向到文件,append模式
  • 上述不等的原因是,重定向从后往前执行

缺点:

  • >>> 不支持 sudo 重定向: 例如当执行 sudo 命令 >文件, 仅 命令 是用 sudo 执行的, 重定向写文件用的是普通用户, 故若此文件仅 root 可写, 则会写入向失败. 可以使用 tee 来解决这个需求

tee [推荐]

基础用法

  • 命令 | tee 文件: write 模式 将命令的标准输出写在文件, 同时输出在终端
  • 命令 | tee -a 文件: append 模式 将命令的标准输出写在文件, 同时输出在终端

变体用法

  • 命令 | tee [-a] 文件 >/dev/null: 仅将命令的标准输出写在文件, 标准输出不输出在终端, 标准错误才输出在终端
  • 命令 | tee [-a] 文件 >/dev/null 2>&1: 仅将命令的标准输出写在文件, 标准输出/标准错误均不输出在终端
  • 命令 2>&1 | tee [-a] 文件: 将命令的标准错误与标准输出写到文件, 同时输出在终端
  • 命令 | sudo tee [-a] 文件 : 普通用户执行命令, sudo 模式重定向其标准输出到文件
  • sudo 命令 | sudo tee [-a] 文件 : sudo 用户执行命令, sudo 模式重定向其标准输出到文件

以上变体均可组合, 如:

  • sudo 命令 | sudo tee [-a] 文件 >/dev/null : sudo 用户执行命令, sudo 模式重定向其标准输出到文件. 仅将命令的标准输出写在文件, 标准输出不输出在终端, 标准错误才输出在终端

优点:

  • 支持写到文件, 同时显示或不显示在终端
  • 支持 sudo 写入到文件

dd

基础用法

  • 命令 | dd of=文件: write模式, 将命令的标准输出写在文件, 其标准输出不输出在终端, 标准错误才输出在终端
  • 命令 | dd of=文件 oflag=append conv=notrunc : append模式, 将命令的标准输出写在文件, 标准输出不输出在终端

变体用法

  • 命令 2>&1 | dd of=文件: write模式, 将命令的标准输出/标准错误写在文件, 均不输出在终端
  • 命令 | sudo dd of=文件: 用普通账号运行命令; write模式, 用 sudo 模式将命令的标准输出写在文件, 标准错误才输出在终端
  • 命令 | sudo dd of=文件: 用 sudo 运行命令; write模式, 用 sudo 模式将命令的标准输出写在文件, 标准错误才输出在终端

缺点:

  • 写到文件的同时无法显示在终端

后台运行

前台运行+重定向输出

前台运行,在这个窗口输新命令无法立即执行

但又看不见输出的结果,输出都写入了文件中

# 输出写到文件,文件原来的内容全删
{
   命令1
   命令2
   ...
} >& 文件名    # 等价于  > 文件名 2>&1

# 输出追加写到文件名的屁股后面
{
   命令1
   命令2
   ...
} >>& 文件名  # 等价于  > 文件名 2>&1
  • > 文件名表示将1(正常输出)写到文件名,文件原来的内容全删
  • >> 文件名表示将1(正常输出)追加写到文件名的屁股后面
  • 2>&1表示将2(错误、警告)都用1(正常输出)输出
  • 文件名 = /dev/null,表示不显示到命令行,也不写到任何真实文件。/dev/null是一个可无限写入、但不保存内容的“黑洞“文件

后台运行结束插播短信

{
   命令1
   命令2
   ...
} &

后台执行命令1,2…

  • 开始启动时显示
[1] 120892
用户名@服务器名 (在此可以干别的事情)
  • 完成时显示
用户名@服务器名 (插播短信"完成进程")
[1]  + 120892 done       { 命令1; 命令2; ... }
用户名@服务器名

后台运行结束不插播短信+输出重定向

( {
   命令1
   命令2
   ...
} & ) >& <log_file>

&:后台执行命令1,2…

>& : 并将错误、警告、正常输出都写到 <log_file>

  • 程序开始和结束都不会有显示,要看输出需开`文件名

后台运行结束插播短信+输出重定向

( {
   命令1
   命令2
   ...
} ) >& <log_file> &

后台执行命令1,2…

  • 开始启动时显示
[1] 120892
用户名@服务器名 (在此可以干别的事情)
  • 完成时显示
用户名@服务器名 (插播"完成进程")
[1]  + 120892 done       { 命令1; 命令2; ... }
用户名@服务器名
  • 其余错误、警告、正常输出都写到 <log_file>
  • 若 <log_file> = /dev/null,表示不显示到命令行,也不写到任何真实文件。/dev/null 是一个可无限写入、但不保存内容的“黑洞“文件。

多进程后台运行+运行完集中输出

rand="$RANDOM"
log_file="/tmp/parallels$rand"
(
    for 范围; do
    {
        命令
    } >> 2>&1 $log_file &
    # &: 多进程后台运行
    # >>  $log_file        # stdout 存入$log_fil
    # >> 2>&1 $log_file    # stdout, warning, error  存入$log_fil
    done
    wait
) >& /dev/null # stdout, warning, error, 包含 后台运行开始和结束的短信
cat $log_file # 集中输出运行结果
rm $log_file

数学运算

$(( 算式 )) 等价于$(expr 算式) 等价于 `xpr 算式`

算式中运算符与数值或变量之间必需有空格, 例如

a=1
echo $((1 + 2));  echo  `expr 1 + 2`;  echo  $(expr 1 + 2);  expr 1 + 2
echo $(($a + 2)); echo  `expr $a + 2`; echo  $(expr $a + 2); expr $a + 2
# 输出皆 3

字符串

Zsh 开发指南(第二篇 字符串处理之常用操作)

以下的 string 可以取做 1 2 …, 表示是函数或脚本的第1,2,…个参数

${1:4} 表示第函数的1个参数的左起第4个字符(0开始计数)及往后的子串

字符串长度

字符串长度 (含颜色控制符)

zsh bash通用

${#string}

字符串长度 (去除颜色控制符)

zsh bash通用, 可以通过如下方法, 去除红色的控制符

string=$(echo 'abc 123abc123' | grep --color=always 123)
length_string=${#string}
# 等价于
string_without_red="$(cat -v <<< "${string}" | sed -E "s/(\^\[\[01;31m\^\[\[K|\^\[\[m\^\[\[K)//g")"
length_string_without_red=${#string_without_red}
echo "$string"
echo "$length_string"
echo "$string_without_red"
echo "$length_string_without_red"

输出

abc 123abc123
47
abc 123abc123
14

字符串取子串

{string:始:长} (从0开始) bash/zsh 支持, {string:始>=0:止<0} 仅zsh支持

bash或zsh支持 zsh支持,bash不支持 bash或zsh里这样写得不到想要的 结果(用python表示)
{string:始:长} {string:始:止}
始>=0 左起
始<=-1 右起
长>=0
始>=0 左起
始<=-1 右起
止<=-1 右起
若始=-m<=-1 且 长>=0 若始=-m<=-1 且 止=-n<0
${string:始} ${string:始:止} string
${string:${ #string}-m:长} string[-m,len(string)-m+长]
${string:${ #string}-m:-n} string[-m,-n]
止=倒数第 n 个 (n>0) **若始>=0 且 止=-n<0 **
${string:始:-n = ${string:始:{ #string}-n-始} ${string:始:-n} string[始:-n]
若始>=0 且 长>=0
${string:始} ${string:始:} zsh无法解析会报错,bash返回空字符串 string[始:]
${string:始:长} string[始:始+长]string[始:min(始+长,len(string)+1]
${string:始:1} ${string:始}结果是 string[始:] string[始]
${string:始:0} ''

说明:

  • ${string:x:y}: x y 可以直接写四则运算,如 ${stinrg:0*1+1:${ #string}/2+1}

$string[始,止] (从1开始) 仅 zsh 支持

本写法zsh支持,bash不支持。始,止皆从1开始,可以是负数。

  • 取一个字符

    $string[n]

    取值:$n \in (-\infty,+\infty)$

    结果:

    if n<0:
        n=max(1, n+len(string))
    if n in range(1, len(string)+1):
        return string[n-1] # 取出的是第n个字符
    else
        return ''          # 取出的是''
  • 取一段字符

    $string[m,n]

    取值:$m,n \in (-\infty,+\infty)$

    结果:

    if m<0:
        m=max(1, m+len(string))
    if n<0:
        n=max(1, n+len(string))
    if m<=n:
        return string[m-1,n]    # 取出的是第[m,n] 区间的字符
    else:
        return ''               # 取出的是''

遍历字符串

以下适用于zsh和bash

for ((i=0; i<${#string}; i++)); do
    echo "${string:$i:1}"
done

字符串拼接

以下适用于zsh和bash

string="${sting1}${string2}"
string+="${sting2}"
string+='xxxxxx'

字符串转数组

string="1:2:3::4 :5"

zsh专用

array=("${(@s/【分割字符串】/)string}")
array=("${(@s/:/)string}")
array=("${(@s/\n/)string}")

bash专用

  • 使用空白字符(正则表达式为\s+, 即连续尽可能多(>=1个)的tab/换行/空格等)分割:

    array=(${string})

    read -a array <<< $string

bash与zsh通用

  • 使用任何单个字符分割

    注意, 连续两个【 分割字符】, 分割开会得到其中间的一个 “” 字符串, 这与bash专用的分割策略不同

    hadopt=false
    [ -n "$ZSH_VERSION" ] && [ "$(setopt | grep shwordsplit)" != '' ] && \
        hadopt=true && setopt sh_word_split # 若为 zsh则开sh_word_split选项
    OLD_IFS="$IFS" ; IFS="【 分割字符】" # " # 必需是单个字符,但可以是汉字
    # 如果是转义字符需加 $'\某',如换行,需要写成  $'\n',必需是单引号
    array=($(echo -E "$string"))
    IFS="$OLD_IFS"
    [ -n "$ZSH_VERSION" ] && [ "$hadopt" = false ] && unsetopt sh_word_split # 若原先没开此选项则关之

    zsh 默认是没有开sh_word_split选项的,没开的话,得到的array=("${string}")

    ​ 执行setopt命令查看开了哪些选项,若见’shwordsplot’则开了此选项

  • 多行字符串, 按换行符分割为数组, 每行开头结尾的连续空白符均保留, 连续两个换行符分割出一个''字符串

    array=()
    while IFS= read -r line ; do
        array+=("$line");
    done <<< "${string}"
    • IFS= 等价于 IFS=‘', 仅改变read这个命令的环境变量, 无需再恢复此环境变量
  • 若不加IFS=, 则line的开头/结尾的连续空白符 (\s+)会被删掉

    • -r: \x 不当成转义字符, 而当成普通字符串, 从而见到"\n”不分割, 见到$'\n’才分割.

    例如

    string='    asdas    12312
    
    asdasd

    (12312后有3个空格)

    array=()
    while IFS= read -r line ; do
    array+=(“$line”);
    done <<< “${string}”
    declare -p array

    
    zsh输出
    

typeset -a array=( ’ asdas 12312 ’ ‘’ asdasd ‘’ ‘’ ‘’ )


bash输出

declare -a array=‘([0]=" asdas 12312 " [1]=“” [2]=“asdasd” [3]=“” [4]=“” [5]=“”)’
    
    是一样的

## 字符串替换

### 引用字符串时替换

以下适用于`zsh`和`bash`

```bash
"${string/旧子串/新子串}"   #  替换首个旧子串
"${string//旧子串/新子串}"  #  替换所有旧子串

  • 新旧字符串转义
    • 空字符串:旧字符串中写作${string/旧子串/} , 不可写作${string/旧子串/''} (这样会替换为'')
    • 空格 新旧字符串均写作`\ ` ,不可写作' '
    • 换行 新字符串中写作\n,旧字符串中写作$'\n'
    • \ 新旧字符串均写作\\
    • $ 新旧字符串均写作\$
    • ` 新旧字符串均写作\`
    • " 新旧字符串均写作\"
  • 新旧字符串不转义
    • 不采用正则表达式
    • '新旧字符串均写作' ,不可写作\'

用sed替换

一下写法适用于mac、linux:

正则表达式进行字符串替换,-E表示支持正则表达式

new_string="`echo $string | sed -E 's/旧子串/新字符串/'`"   # 替换第一个匹配
new_string="`echo $string | sed -E 's/旧子串/新字符串/g'`"  # 替换第全部匹配

输出文件内容并用正则表达式替换

cat 文件 | sed -E 's/旧子串/新字符串/' # 替换第一个匹配
sed -E 's/旧子串/新字符串/' 文件  # 替换第一个匹配
cat 文件 | sed -E 's/旧子串/新字符串/g' # 替换第全部匹配
sed -E 's/旧子串/新字符串/g' 文件 # 替换第全部匹配

更多参考

  • -E才能在mac和linux下都支持正则表达式,不加-E则只linux支持正则表达式

  • 新旧字符串中若有/,命令里应该写作\/,不能写作"/",'/',\\\/,/

  • 支持[a-zA-z0-9]的写法

  • [a-z]+:加了-E则 应该写作 [a-z]+ (不加-E写作[a-z]\+)

  • 空白字符:不支持\s(空白字符),支持[:space:]

    • 例如替换所有空白字符sed -E 's/[[:space:]]+//g'
    • 例如替换所有非空白字符sed -E 's/[^[:space:]]+//g'
  • 换成换行符:

    • 应该写成 's/旧子串/新字符串前段'$'\\\n''新字符串后段/g'

    • 或写成

      's/旧子串/新字符串前段\
      新字符串后段/g'
    • 不可写成s/旧子串/新字符串前段\n新字符串后段/g ,或s/旧子串/新字符串前段\\\n新字符串后段/g

  • 跨行替换/将换行符换掉:如些写

    sed -e ':a' -e 'N' -e '$!ba' -Ee 's/旧子串前段\n旧子串后段/新字符串/g'
  • sed的参数 -e 'xxxx' 表示追加一条命令,即

    • sed只执行一条命令(如s/xxx/xxx/g), 则 sed "s/xxx/xxx/g"sed -e "s/xxx/xxx/g"的简写
    • sed执行多条命令时,则每条命令前需要写-e,如sed -e 'xx' -e 'xxx' -e 'xxxxx'
  • 引用被匹配的内容:sed 's/xxx\(aaa\)xxx\(bbb\)xxx/yyy\1yyy\2yyy/g',则替换结果为

    yyyaaayyybbbyyy

用perl替换

sed不支持正则表达式中的向前匹配、向后匹配、非贪婪匹配(即模式后面加?),可以用perl命令代替sed搞替换。

Linux系统一般默认安装Perl包

以下适用于zshbash

perl -pe 's/旧子串/新子串/g' <文件路径> # 输出替换后的全文
perl -i -pe 's/旧子串/新子串/g' <文件路径> # 在文件内直接替换
echo "${string}" | perl -pe 's/旧子串/新子串/g' <文件路径> # 替换输出
cat <文件路径> | perl -pe 's/旧子串/新子串/g' <文件路径> # 替换输出

取第几行

取前m行

echo "${string}" | head -n <m>

取后行

echo "${string}" | tail -n <m>

去掉前m-1

echo "${string}" | tail -n +<m>

去掉后m行

echo "${string}" | head -n -<m>

截取

语法

写法 含义
${variable#pattern} 去掉variable头最短匹配的pattern
${variable##pattern} 去掉variable头最长匹配的pattern
${variable%pattern} 去掉variable尾最短匹配的pattern
${variable%%pattern} 去掉variable尾最长匹配的pattern

pattern 采用通配符的语法:

  • ? : 0或1个任意字符
  • * : [0,+∞)个任意字符
  • [...] : 匹配一个字符 中括号里面
  • [!...] (bash), [\!...] (zsh): 匹配一个字符 不在中括号里

注意:

  • [...][!...] 后加+ * ? 均不表示对中括号描述的字符重复[1,+∞), [0,+∞), [0,1]次; 而是表示匹配完一个中括号描述的字符, 而后再匹配 一个"+”字符/[0,+∞)个任意字符/0或1个任意字符

用法

截取某个子串之前/之后

"${string%%子串*}"    # 截取 string 第一个 <子串> 之前
"${string%子串*}"     # 截取 string 最后一个 <子串> 之前
"${string#*子串}"     # 截取 string 第一个 <子串> 之后
"${string##*子串}"    # 截取 string 最后一个 <子串> 之后

注释:

  • 子串 可以直接写字符, 可以是单个或多个字符, 如

    string='https://github.com/someone://other'
    echo "${string%%://*}"
    echo "${string#*://}"

    返回

    https
    github.com/someone://other
  • 子串 可以写 ${substring}

    string='https://github.com/someone://other'
    substring='://'
    echo "${string%%${substring}*}"
    echo "${string#*${substring}}"

    也返回同上

  • 若无目标子串, 则各自均换返回整个字符串, 例如

    string='https://github.com/someone://other'
    substring='///'
    echo "${string%%${substring}*}"
    echo "${string#*${substring}}"

    返回

    https://github.com/someone://other
    https://github.com/someone://other

调用

bash与zsh通用的写法是 "${string}”"$string", 加不加{...}无影响

bash

bash中, 调用变量名为string的字符串, 必需两侧加引号.

"$string"

若不加引号, 则其中所有连续大于等于1个空白符(即正则表达式的\s+)均替换为一个空格

且在echo时, 行开头与结尾的连续空白符(即^\s+)会删去. 例如

a='   a b  c   d
e
f  ' # d后有四个空格, 可能代码里显示不出来
echo $a'|'
a b c d e f |
echo '|'$a'|'
| a b c d e f |
echo "$a|"
   a b  c   d
e
f  |
echo "$a|"
|   a b  c   d
e
f  |

zsh

zsh中, 调用变量名为string的字符串, 加不加引号皆可, 均会输出完整的字符串, 各行齐全, 空白符未改动.

即 zsh中 "$string”$string 结果都是 与bash中的 "$string” 相同的.

输出

以下适用于zshbash

echo -E 字符串 # 输出不转义,即`\某`不会转义,输出还是`\某`
echo -e 字符串 # 输出会转义,即`\某`会转义
echo -n 字符串 # 结尾不换行

输入

参数含义:输入写到string变量

read -r string # 输入不转义,即`\某`不会转义,输入还是`\某`;输入完会换行
read -s string # 输入字符不显示;输入完不会换行
read -p 提示信息 string # bash支持,zsh不支持

组合用法:以下适用于zshbash

  • 输入普通信息

    echo -n 提示信息; read -r string;
    提示信息:输入内容
    输入完进入下一行
  • 输入密码

    echo -n 提示信息; read -rs string; echo '';
    提示信息:                       // 输入内容不显示
    输入完进入下一行

转义

以下适用于zshbash

  • ' in 'xxxx''xxx'\''xxxx'(推荐) 或 'xxx'"'"'xxx'
    • 'xxx\'xxxx' 无用,会被解析成xxx\(字符串),xxxx(变量), '(另一个字符串的开头)
  • " in "xxxx": "xxx"\""xxxx""xxx\"xxx"(推荐)

数组

参考

显示数组

declare -p array

返回如下

zsh

typeset -a array=( 1 '2\n' 3 '4 ' 5 )

bash

declare -a array='([0]="1" [1]="2\\n" [2]="3" [3]="4 " [4]="5")'

数组取元素

${array[@]:始:长} (从0开始) sh/bash/zsh共享

  • array数组 起始数字从 0 开始, 长度>=1; 函数的参数 起始数字从 1 开始, 长度>=1
# array数组                            函数的参数
"${array[@]:起始数字:1}"               "${@:起始数字:1}"      # return array[起始数字]
"${array[@]:起始数字:长度}"             "${@:起始数字:长度}"   # return array[起始数字:长度]
"${array[@]:起始数字}"                 "${@:起始数字}"        # return array[起始数字:]
"${array[@]}"                         "${@}"               # return array
"${array[@]:${#array[@]}}"            "${@:$#}"            # return array[-1]
"${array[@]:${$((#array[@] - 1))}}"   "${@:$(($# - 1))}"   # return array[-2:]     $(( 算式 )) 等价于 `expr 算式 `
"${array[@]:${$((#array[@] - 1))}:1}" "${@:$(($# - 1)):1}" # return array[-2:]     $(( 算式 )) 等价于 `expr 算式 `
  • , 为变量时, 必须写$, 例如

    "${array[@]:$index:2}"    # 可以解析
    "${array[@]:index:2}"     # 无法解析
  • , 为四则运算时, 直接写算式即可, 支持+ - * / ( ), 例如

array=(zero one two three four five six seven eight nine ten )
idx=3
echo ${array[@]:(($idx+2)/2)*3-1:1}
# 返回 5

$数 (从1开始) sh/bash/zsh共享

"$数"                     # 函数的第[数]个参数
  • 为变量时, 只能用eval var=\"\${${idx}}\" 来获得 (如下), 或用 ${@:$idx:1} (必需写$)

    f() {
        idx=2
        echo $idx  # 返回 2
        eval var=\"\${${idx}}\"  ;	echo ${var}  # 返回  arg 2
        echo ${${idx}} # 返回 2
        echo ${$idx} #  返回 f:5: bad substitution
    }
    
    f 'arg 1' 'arg 2'
  • 为size运算算式时, 只能用eval var=\"\${$((算式))}\" 来获得 (如下), 或用 ${@:直接写size运算:1} (必需写$)

    f() {
        eval var=\"\${$((1 + 1))}\"
        echo $var
    }
    f 'arg 1' 'arg 2'
    # 返回 arg 2

"${array[数]}” (bash从0开始, zsh从1开始)

  • sh/bash: 数字 从0开始
  • zsh: 数字从1开始
  • "${@[数字]}” 写法无效
"${array[数字]}"           # return array[数字]
  • 为变量时, 可以不写$, 例如

    "${array[@]:$index:2}"    # 可以解析
    "${array[@]:index:2}"     # 可以解析
  • , 为四则运算时, 直接写算式即可, 支持+ - * / ( )

    array=(zero one two three four five six seven eight nine ten )
    idx=3
    echo ${array[(($idx+2)/2)*3-1]}
    # bash 返回 five, zsh 返回 four

${array[始,止]} (从1开始) 仅zsh支持

  • 数字/起始数字 从1开始, 表示正着数
  • 起始数字, 结束数字可为-1,-2, …, 表示结束数字倒着数
  • 不论正着数还是倒着数, 只要起始位置比结束位置靠右, 则返回空白字符串
  • , 为变量时, 必须写$
  • , 为四则运算时, 直接写算式即可, 支持+ - * / ( )
# array数组                  函数的参数
"${array[数字]}"             "${@[数字]}"              # 按照上述index计法, 返回第 数字 个元素, 对应Python: array[数字-1]
"${array[起始数字,结束数字]}"  "${@[起始数字,结束数字]}" # 按照上述 index 计法, 返回数组 取闭区间 [起始数字,正结束数字]

${array[起始数字,结束数字]} 对应的 python 代码

if 结束数字>0:
    return array[起始数字-1:结束数字]
else if 结束数字==0:
    return []
else if 结束数字==-1:
    return array[起始数字-1:]
else:
    return array[起始数字-1:结束数字+1]

数组长度

zsh/bash通用

${#array[@]}

zsh专用

${#array}
# 或
$#array

判断是否是数组

封装成函数

is_array() {
    local array_name="$1"
    # declare -p $array_name
    if [[ "$(declare -p $array_name)" =~ 'declare -a ' ]] || \
       [[ "$(declare -p $array_name)" =~ 'typeset -g -a ' ]] || \
       [[ "$(declare -p $array_name)" =~ 'typeset -a ' ]] ; then
        echo true
    else
        echo false
    fi
}
a=(gpu{1..3})
is_array a
true
a=adsaasd
is_array a
false

注:

  • bash 返回

    declare -a array_name='([0]="gpu1" [1]="gpu2" [2]="gpu3")'
  • zsh 返回

    typeset -g -a array_name=( gpu1 gpu2 gpu3 )
    或
    typeset -a array_name=( gpu1 gpu2 gpu3 )

数组赋值与拼接

以下适用于zshbash

  • 数组赋值
array=("${array1[@]}")

declare -p arraydeclare -p array1输出结果会完全一样,即array和array1的每个元素都一样,元素内可以含有空格、换行,均不影响赋值。

  • 数组拼接并赋值
array=("${array1[@]}" "${array2[@]}")
array=("${array1[@]}" "string")
array=("string" "${array2[@]}")
array+=("${array2[@]}")
# 等价于
array=("${array[@]}" "${array2[@]}")

array1、array2元素内可以含有空格、换行,均不影响拼接和赋值。

上述不可省略",这是因为

  • ${array[@]}$(bash) 返回 '\n'.join(array).split('\n')(python)

  • "${array[@]}$" (zsh、bash) 和 ${array[@]}$(zsh) 返回 array(python)

    例如

    array1=('asd' 'asd\nsads
    asdsa')
    array2=('asd')
    array=(${array1[@]} ${array2[@]})
    declare -p array
    • bash中返回

      declare -a array='([0]="asd" [1]="asd\\nsads" [2]="asdsa" [3]="asd")'

      array数组成员中若有换行, ${array[@]}$ 会得到两个数组成员,在换行号处断开成

    • zsh中

      typeset -a array=( asd $'asd\nsads\nasdsa' asd )

      array数组成员中若有换行, ${array[@]}$ 不会得到两个数组成员,不在换行号处断开成

生成range

以下适用于zshbash

一组range

array=('前缀'{起始数字..结束数字}'后缀')  # 前缀、后缀可有空格、回车

原理:

  • 代码前缀{起始数字..结束数字}后缀等价于代码'前缀xx后缀' '前缀xx后缀' …… '前缀xx后缀'
  • ('前缀'起始数字..结束数字}'后缀')返回数字('前缀xx后缀' '前缀xx后缀' …… '前缀xx后缀')

多组range拼接起来

array=('前缀1'{起始数字..结束数字}'后缀1' '前缀2'{起始数字..结束数字}'后缀2')  # 前缀、后缀可有空格、回车

原理:

  • 等价于

    array=('前缀1xx后缀1' '前缀1xx后缀1' …… '前缀1xx后缀1' '前缀2xx后缀2' '前缀2xx后缀2' …… '前缀2xx后缀2')

从文件加载数组

适用于zsh、bash,mac、linux

  • 以换行为分隔符(tab和空格皆不分割)
IFS_old=$IFS
IFS=$'\r\n'
array=($(<file_path))
IFS=$IFS_old
  • 以空格为分隔符(tab和换行皆不分割)
IFS_old=$IFS
IFS=' '
array=($(<file_path))
IFS=$IFS_old
  • 以空白字符为分割(空格、tab、换行皆分割)
array=($(<file_path))

遍历数组

遍历元素

正确写法:以下适用于zshbashsh

# 自定义的数组
for idx in "${array[@]}"; do
    ...
done
# 传入函数或文件的参数
for idx in "$@"; do
    ...
done

# 遍历数组 (1 2 3 4)
for idx in 1 2 3 4; do
    ...
done
# 遍通配符路径
for idx in ./*.zip ../*; do
    ...
done

错误写法:

  • 以下写法,zsh 会正常遍历数组;bash 只遍历到数组首个元素就结束了遍历。
# 自定义的数组
for idx in $array; do
    ...
done

# 传入函数或文件的参数
for idx in $@; do
    ...
done

# 遍历数组 (1 2 3 4)
for idx in (1 2 3 4); do
    ...
done

# 遍通配符路径
for idx in $(ls ./*.zip ../*); do
    # 这样会把 被匹配到的文件夹向下列出一级, 例如
    # house.zip  object_vox.zip  object.zip  room.zip  ../suncg_data.zip  texture.zip

    # ../suncg_data:
    # house  object  object_vox  room  texture

  # ../suncg_data_old:
  # house  object  object_vox  room  suncg_room_json  suncg_room_obj  texture

  # ../suncg_data.zip_extract:
  # house.zip  object_vox.zip  object.zip  room.zip  texture.zip
    ...
done

遍历编号

以下适用于zshbashsh

for ((i = 0; i < ${#array[@]}; i++)); do
    echo $i ${array[@]:$i:1}
    # 不要使用 ${array[i]}, bash和sh的是i从0开始,zsh的是从1开始
done

以下适用于bashsh

for i in "{!array[@]}"; do
# bash和sh的是i从0开始
    echo $i ${array[@]:$i:1}
    # 或
    echo $i ${array[i]}
done

命令返回数组

法一 for + $(命令)`命令`

  • zsh/bash中返回一个字符串(不论多少行),输入给函数或for,则会自动用空格分割。
  • 而不是返回数组(自带切分,而不是以空格或换行符切分)。

例:在一个文件夹下有file1 file with space 有空格的 中文文件

# 返回给for
for i in $(ls); do
    echo "|$i|"
done
# 或返回给函数
f() {
    for i in "$@"; do
        echo "|$i|"
    done
}
f $(ls)

在bash下返回

|file1|

| file with space|

|有空格的 中文文件|

在zsh/bash下返回

|file1|
|file|
|with|
|space|
|有空格的|
|中文文件|

  • 解决办法:将命令结果分行返回( 如ls -1 一个文件返回一行),然后设置IFS=$'\n' ,则返回给for函数的均是数组

    hadopt=false
    [ -n "$ZSH_VERSION" ] && [ "$(setopt | grep shwordsplit)" != '' ] && \
        hadopt=true && setopt sh_word_split # 若为 zsh则开sh_word_split选项
    OLD_IFS="$IFS"; IFS=$'\n'
    # ---
    # 返回给for
    for i in $(ls -1); do
        echo "|$i|"
    done
    # 或返回给函数
    f $(ls)
    # ---
    IFS="$OLD_IFS"
    [ -n "$ZSH_VERSION" ] && [ "$hadopt" = false ] && unsetopt sh_word_split # 若原先没开此选项则关之

    结果

    > |file1|
    >
    > | file with space|
    >
    > |有空格的 中文文件|

## 数组传入传出函数

以下适用于zsh和bash

### 法一 `传入"${array[@]}"`

以下适用于zsh和bash,向文件或函数传入数组

**传入数组**

写法:

* 传入一个数组

​```bash
zsh/bash 文件.sh "${array[@]}"
zsh/bash  文件.sh "$@"
func "${array[@]}"
func "$@"
  • 传入多个数组(以函数为例,其余同理成立)
func "string1" "${array1[@]}" "string2" "${array2[@]}" "string3"

“${array[@]}” 必需得加引号,若不加引号,则会如下出错:

file.zsh内容为

for i in "$@"; do
    echo "|$i|"
done

而在bash中运行函数

f(){ zsh file.sh $@ }
f asdas 'qwe qweasd'

会输出

|asdas|

|qwe|

|qweasd|

例子

get_array(){
    local array=("$@") # 把传入变成数组
  declare -p array # 检测
}
go_through_arg () {
    for idx in "$@"; do
    # 遍历输入,若输入有 `go_through_arg 'sad asd' 'asd' adasd`
    # bash 中不支持 写作 $@,$*,"$*"
    # zsh 中不支持写作 "$*",但支持写作$@,$*
    # zsh和bash 仅 "$@"通用,可输出三行,分别是'sad asd','asd', 'adasd'
        echo "|$idx|"
    done
}
argparse () {
        echo "|$1|"
        echo "|$2|"
    echo "|$3|"
}

array=("a" "b asd" "c\nasd
asdsa")

传参写法

get_array "${array[@]}" # 传入数组
typeset -a array=( a 'b asd' 'c\nasd\nasdsa' )
go_through_arg "${array[@]}" # 传入数组
|a|
|b asd|
|c
asd
asdsa|
argparse "${array[@]}" # 传入数组
|a|
|b asd|
|c
asd
asdsa|

法二 用declare -p array作为表示

以下适用于zsh和bash

缺点:依赖于zsh的版本的操作系统

传出数组

make_array(){
    local array
  declare -a array
  array=('a' 'b asd' 'c\nasd\nasds
  asdas')
  echo -E ${"$(declare -p array )"#*=} #
}
make_array

mac zsh 5.7.1 (x86_64-apple-darwin18.2.0) 返回,不能使用本法

( a 'b asd' 'c\nasd\nasdsa' )

Linux zsh 5.4.2 (x86_64-ubuntu-linux-gnu)返回 ,能使用本法

( a 'b asd' $'c\\nasd\\nasds\n  asdas' )

Linux zsh 5.0.5 (x86_64-pc-linux-gnu) 返回 ,能使用本法

(a 'b asd' 'c\nasd\nasds
asdas')

传入数组

get_array(){
    local getton_array
    declare -a getton_array
  eval "getton_array="$1 # get array
  declare -p getton_array
}
get_array "$(make_array)"

mac zsh 5.7.1 (x86_64-apple-darwin18.2.0) 返回,不能使用本法

typeset -a getton_array=( a 'b asd' $'c\nasd\nasds\n  asdas' )

Linux zsh 5.4.2 (x86_64-ubuntu-linux-gnu)返回 ,能使用本法

typeset -a getton_array=( a 'b asd' $'c\\nasd\\nasds\n  asdas' )

Linux zsh 5.0.5 (x86_64-pc-linux-gnu) 返回 ,能使用本法

typeset -a getton_array
getton_array=(a 'b asd' 'c\nasd\nasds
asdas')

法三 修改分隔符

以下适用于zsh和bash

a2s() {
    local string=''
    if [ $# -ne 0 ]; then
        string="$1"
        shift
    fi
    for i in "$@"; do
        string="$string$i" # 可以用任意字符,但只能是单个字符,故选用冷门汉字
    done
  echo -E "$string"
}
make_array() {
    local array
  declare -a array
  array=('a' 'b asd' 'c\nasd\nasdss
  asdas')
  a2s "${array[@]}"
  # echo -E "$(a2s \"${array[@]}\")"
}
make_array

返回

a䴅b asd䴅c\nasd\nasdss
asdas
get_array(){
  local str="$1"
  local OLD_IFS=$IFS
  IFS='䴅'
  local array=($(echo -E "$str"))
  # local array=($str)
  IFS=$OLD_IFS
  declare -p array
}
get_array "$(make_array)"

返回

typeset -a array
array=( 'c\nasd\nasdss
asdas' )

引用传参——在函数中修改参数

适用于 zsh 与 bash

f() {
  local a_="$1"
  eval $a_='asdsad' # 注意,这里的`a_`不要和传入`f`的值`a`重名
}
# 或
f() {
  eval $1='asdsad'
}
# 或
f() {
  local a_="$1"
  local c='asdsad'
  eval $a_=\"\$c\"
}
# 或
f() {
  local c='asdsad'
  eval $1=\"\$c\"
}
# 而后
test() {
  local a=123
  f a # 注意,这里的`a`不要和`f`中的`a_`重名
  echo $a
}
test
asdsad

其原理是eval $a_=\"\$c\" 等价于执行了a="$c"

运行数组

错误的做法

  • 方法一

    eval "${array[@]}"  # 即等价于 python 中的 sys.command(' '.join(array))
    • 这样会把含有空白字符的字符串拆开
  • 方法二

    "${array[@]}"
    • 当array的第一个元素不是终端的命令时(如第一个元素为CUDA_VISIBLE_DEVICES=1),则无法执行
    • 当array中有元素等于|;{} && ||等时,也无法执行

正确的做法

本方法可以克服上述法一、二的所有问题

evalarray() {
    if [ $# -eq 1 ]; then
        # 若array仅一个元素,则直接 sys.command(array[0])
        eval "$1"
    else
        # 否则 sys.command( ' '.join([
        #	x.replace("'", "'\''")
        #    if ' ' in x or  '\n' in x or  '\t' in x
        #    else x
        #    for x in array]
        # ) )
        local processedarray=()
        for arg in "$@"; do
            if [[ "$arg" =~ ' ' ]] || [[ "$arg" =~ $'\t' ]] ||  [[ "$arg" =~ $'\n' ]]; then
                # 若arg中有空白字符,则arg两端加',arg中间的'改为'\''
                processedarray+=("'${arg//'/'\\''}'")
            else
                processedarray+=("${arg}")
            fi
        done
        eval "$processedarray[@]"
    fi
}

使用

evalarray "${array[@]}"

例如:

  • 用法一:所有命令和参数均分开写。当参数含有空白字符和特殊运算连接符(|;{} && ||等)时,才用括号括起来。
array=(touch '文件名 有空格' '&&' ls .. '|'  grep a)
evalarray "${array[@]}"
# 或
evalarray touch '文件名 有空格' '&&' ls .. '|'  grep a
evalarray touch 文件名\ 有空格 '&&' ls .. '|'  grep a
  • 用法二:所有命令和参数写在一个字符串中
array=('touch 文件名\ 有空格 && ls .. |  grep a')
evalarray "${array[@]}"
# 或
evalarray 'touch 文件名\ 有空格 && ls .. |  grep a'
  • 注意,用法一二不能混着写,例如,
evalarray touch '文件名 有空格' '&&' ls .. '|'  'grep a'

这样不行,运行时会把’grep a’当成一个终端命令,故而报错

(eval):1: command not found: grep a

输出表格

column 命令

代码块 | column -t -s 分割字符的集合 即能输出表格, 使得各列左对齐

  • 例1: 单个分隔字符:

    注意必需写成$'\t’ 才会转义成制表符, 用它分隔列; 写成"\t'\t’ 均不转义, 而是用\t 这两个字符分列

    echo 'Name\tScore
    -----\t-----
    Somebody with a very long name\t100
    ShortNmae\t1231981738184788381413131
    Ann\t
        \tnana' | column -t -s $'\t'

    输出

    Name                            Score
    -----                           -----
    Somebody with a very long name  100
    ShortNmae                       1231981738184788381413131
    Ann
                                    nana
  • 例2: 多个分隔字符

    echo 'Name|Score\n-----|-----
    Somebody with a very long name\t100
    ShortNmae\t1231981738184788381413131
    Ann|
        |nana' | column -t -s $'\t|'

    输出同例1

sudo执行

sudo执行非sudo的函数

sudo bash -c "$(declare -f <非sodu的函数名>); <非sodu的函数名>"

sudo执行代码段

sudo bash <<EOF
代码段
EOF

sudo执行文件

sudo bash <文件路径>