How to Write Shell Scripts

The basic grammer of shell/Bash script.

ref: shell tutorial

How to Write Shell Script

首先做个澄清:Shell 是一类程序的统称,其中最流行的是 Bash Shell,所有 Linux 发行版和 MacOS 都安装了它;除此之外,zsh 也很流行。考虑到 Bash 的流行程度以及其他更复杂的 Shell 的功能基本是 Bash 的超集,因此我们这里只介绍 Bash Shell 脚本。

前置知识:Shell 是如何执行命令的

Shell 在执行命令时,通常先 fork 出一个子进程,再由子进程 exec 目标程序。

这里有两个常见例外:

  1. Shell 内建(built-in)命令,例如 cdexport,通常直接在当前 Shell 进程中执行。(但不包括 lsls不是内建命令)
  2. 使用 source 执行脚本,例如 source any-script.sh,脚本内容会直接在当前 Shell 中执行。

Shell 中的变量可以分为普通 Shell 变量和环境变量。普通变量不会被子进程继承(或者说复制);环境变量会被复制给子进程。这也意味着子进程修改自己的环境变量不会影响父进程。

定义方式如下:

# 普通变量
FOO=hello

# 环境变量
export FOO=hello

前置知识:执行shell脚本的具体方式

执行脚本文件:

  1. 直接指定sh来执行该脚本,不需要shebang,也不需要脚本有执行权限(因为该脚本直接作为参数传给了sh).

    sh script_file_path
  2. 需要写shebang来指定解释器, 并且要指定脚本路径.

    必须加上./使得该command name被识别为一个路径名. 否则shell会继续在alias, builtin和PATH中搜索该command name. 1

    chmod +x script_file ##(chown, chgrp optionally)
    ./script_file
  3. 使用.source在当前shell session中执行该脚本

    source script_file

    or

    . script_file

    因此, 该方法可以用于刷新当前shell环境:

    source ~/.bashrc
  4. Shell script是能在命令行直接输入的, 但仅会作用一次

注意: 方法1,2都是新开一个子shell session,在其中执行脚本,而方法3, 4是在当前shell session中执行脚本

Environment virable

$PATH详见Shell Script Searching Path


常见的环境变量:

环境变量 说明
$HOME 当前用户的登陆目录
$PATH 以冒号分隔的, 由多个路径所组成的, 用来搜索命令的列表
$PS1 命令行提示符,通常是”$”字符 (很多主题都会改掉$PS1)
$PS2 辅助提示符,用来提示后续输入,通常是”>”字符
$IFS 输入区分隔符。当shell读取输入数据时会把一组字符看成是单词之间的分隔符,通常是空格、制表符、换行符等
#我们在当前Shell进程中指定了var1变量
[roc@roclinux ~]$ var1="hello"
[roc@roclinux ~]$ echo $var1
hello

#我们在当前Shell进程中又指定了var2变量
[roc@roclinux ~]$ var2="world"
[roc@roclinux ~]$ echo $var2
world

#我们通过export发布var1
[roc@roclinux ~]$ export var1

#我们进入到一个bash子进程中
[roc@roclinux ~]$ bash

#var1变量仍然有效, 而var2变量已经无效了
[roc@roclinux ~]$ echo $var1 # 输出变量的值
hello
[roc@roclinux ~]$ echo $var2 # 什么也没有输出

例子:用 source 执行会更改环境变量的脚本

假设有一个脚本 reset_env.sh

export FOO=new_value

如果直接执行:

export FOO=old_value
./reset_env.sh

echo "$FOO"

输出仍然是:

old_value

因为 reset_env.sh 在子进程中执行,修改的是子进程自己的环境。

如果改成:

source reset_env.sh

echo "$FOO"

输出则是:

new_value

因为 source 会在当前 Shell 中执行脚本,所以能够直接修改当前 Shell 的环境变量。

Parameter variable

  • $0 - Name of the script
  • $1 to $9 - Arguments to the script. $1 is the first argument and so on.
    • n>=10时,需要使用${n}来获取参数
  • $@ - 全部参数组成的列表
  • $# - Number of arguments
  • $? - Return code of the previous command
  • $*: 全部参数连接成的字符串,按$IFS的第一个字符分割
  • $$ - Process identification number (PID) for the current script
  • !! - Entire last command, including arguments. A common pattern is to execute a command only for it to fail due to missing permissions; you can quickly re-execute the command with sudo by doing sudo !!
  • $_ - Last argument from the last command. If you are in an interactive shell, you can also quickly get this value by typing Esc followed by . or Alt+.

Variable

  • assign variables in bash: foo=bar

    • Note that foo = bar will not work since it is interpreted as calling the foo program with arguments = and bar. In general, in shell scripts the space character will perform argument splitting.
      • 因此不要加空格
  • access the value of the variable: $foo

    • 等价于 ${foo}, 花括号可以精确地界定变量名称的范围。
  • 可以用read命令从标准输入接受数据并赋值:read val

    example:

    #! /usr/bin/env bash
    echo -n "Enter your name:"
    read name
    echo "hello $name"
    exit 0

String

  • Strings in bash can be defined with ' and " delimiters, but they are not equivalent.

  • Strings delimited with ' are literal strings and will not substitute variable values whereas " delimited strings will.

    foo=bar
    echo "$foo"
    # prints bar
    echo '$foo'
    # prints $foo

Array

  • array initialization −

    array_name=(value1 ... valuen)
  • assign

    array_name[index]=value
  • access Array Values

    ${array_name[index]}

Quoting Mechanism

metacharacters

Unix Shell provides various metacharacters which have special meaning while using them in any Shell Script and causes termination of a word unless quoted.

* ? [ ] ' " \ $ ; & ( ) | ^ < > new-line space tab

quoting

The following table lists the four forms of quoting −

Sr.No. Quoting & Description
1 Single quote
All special characters between these quotes lose their special meaning.
2 Double quote
Most special characters between these quotes lose their special meaning with these exceptions −$`$'"\
3 Backslash
Any character immediately following the backslash loses its special meaning.
4 Back quote (aka backtick)
Everything you type between backticks is evaluated (executed) by the shell before the main command, and the output of that execution is used by that command,
  • The Single Quotes: 其内容不转义, 相当于在每个字符前加 backslash

  • The Double Quotes: 其内容转义

  • The Backslash: 取消其后面的一个字符的转义

  • The Backquotes:将其内容视作 command 并执行, 与后文的CMD substitution类似

    var=`command`
    DATE=`date`

    echo $DATE

    #等价于后文的CMD substitution
    echo $(DATE)

常见语法

条件语句

  • As with most programming languages, bash supports control flow techniques including if, case, while and for.

if

syntax:

if [ expression ] 
then
Statement(s) to be executed if expression is true
fi
if [ expression 1 ]
then
Statement(s) to be executed if expression 1 is true
elif [ expression 2 ]
then
Statement(s) to be executed if expression 2 is true
elif [ expression 3 ]
then
Statement(s) to be executed if expression 3 is true
else
Statement(s) to be executed if no expression is true
fi

紧凑形式: ; (同一行上多个命令的分隔符)

example:

if [ -f ~/.bashrc ]; then 
. ~/.bashrc fi
#!/bin/sh
read -p "Is this morning? Please answer yes or no: " answer
if [ "$answer" = "yes" ]; then
echo “Good morning”
elif [ "$answer" = "no" ]; then
echo “Good afternoon”
else
echo “Sorry, $answer not recognized. Enter yes or no”
exit 1
fi
exit 0

case

syntax:

case word in
pattern1 | pattern2)
Statement(s) to be executed if pattern1 matches
;;
pattern3)
Statement(s) to be executed if pattern2 matches
;;
pattern4)
Statement(s) to be executed if pattern3 matches
;;
*)
Default condition to be executed
;;
esac

example:

#!/bin/sh

option="${1}"
case ${option} in
-f) FILE="${2}"
echo "File name is $FILE"
;;
-d) DIR="${2}"
echo "Dir name is $DIR"
;;
*)
echo "`basename ${0}`:usage: [-f file] | [-d directory]"
exit 1 # Command to come out of the program with status 1
;;
esac
#!/bin/sh

read -p "Is this morning? Please answer yes or no." answer
case "$answer" in
yes | y | Yes | YES)
echo “Good morning!”
;;
no | n | No | NO)
echo “Good afternoon!”
;;
*)
echo “Sorry, $answer not recognized.”
;;
esac
exit 0

select in && case in

select in语句自带循环

select variable in value_list
do
statements
done
  • variable: 表示变量
  • value_list: 取值列表
  • in: Shell关键字

select in 通常和 case in 一起使用,在用户输入不同的编号时可以做出不同的反应

example:

#!/bin/sh
clear
select item in Continue Finish
do
case "$item" in
Continue)
;;
Finish)
break
;;
*)
echo "Wrong choice! Please select again!"
;;
esac
done

该命令的while版本:

while [ "$item" != "Finish" ]; 
do
read item
case "$item" in
"Continue")
;;
"Finish")
;;
*) echo "Wrong choice! Please select again!"
;;
esac
done

循环语句

break: 从for/while/until循环退出

for

syntax:

for var in word1 word2 ... wordN
do
Statement(s) to be executed for every word.
done

example:

for FILE in $HOME/.bash*
do
echo $FILE
done
for f in *.png             
do
mv -n $f $f.kk
done

while

syntax:

while condition 
do
statements
done

example:

quit=n
while [ "$quit" != "y" ]; do read menu_choice
case "$menu_choice" in
a) echo "a, continue...";;
b) echo "b, continue...";;
q|Q) quit=y;;
*) echo "Sorry, choice not recognized.";;
esac done
a=0
LIMIT=10
while [ $a -le $LIMIT ]
do
a=$(($a+1))
if [ $a -gt 2 ]
then
break # Skip entire rest of loop.
fi
echo -n "$a"
done

命令组合

分号串联:

command1 ; command2 ; ...

条件组合:

statement1 && statement2 && statement3 && ...
statement1 || statement2 || statement3 ||

语句块

{
statement1
statement2
...
}

{ 
statement1; statement2 ; ... ;
}

test expression 或 [ expression ]

  • [ (aka test) command the and [[ ... ]] test construct are used to evaluate expressions

  • [是一条命令, 与test等价,大多数shell都支持。在现代的大多数sh实现中,[test是builtin命令

    • []将其operand直接当作argument
  • [[,是关键字,许多shell(如ash bsh)并不支持这种方式

    • [[]]将其operand进行参数引用,算术扩展和CMD substitution, 不需要手动转义等

Function

syntax

function_name(){
list of commands
[ return value ]
}

example:

mcd () {
mkdir -p "$1"
cd "$1"
}

return

  • exit: 不仅会退出函数, 还会退出执行该函数的shell
  • return code:仅仅退出函数. 和command的return code同

return code

  • Commands have Return Code to report errors in a more script-friendly manner.
    • 0 usually means everything went OK; anything different from 0 means an error occurred.
    • return code, 你可以指定返回任何值

return code as bool value:

  • Return codes can be used to conditionally execute commands using && (and operator) and || (or operator) .

  • Commands can also be separated within the same line using a semicolon ;.

  • The true program will always have a 0 return code and the false command will always have a 1 return code.

    false || echo "Oops, fail"
    # Oops, fail

    true || echo "Will not be printed"
    #

    true && echo "Things went well"
    # Things went well

    false && echo "Will not be printed"
    #

    true ; echo "This will always run"
    # This will always run

    false ; echo "This will always run"
    # This will always run

Shebang

Note that scripts need not necessarily be written in bash to be called from the terminal. For instance, here’s a simple Python script that outputs its arguments in reversed order:

#!/usr/local/bin/python
import sys
for arg in reversed(sys.argv[1:]):
print(arg)
  • shebang: the character sequence consisting of #! at the beginning of a script in a Unix-like operating system

    • shell会将shebang中#!之后的内容作为一个程序的路径,打开该程序, 将本script的路径当作参数传入( 即: 将整个script当作input传入shebang所指定的程序 )

    • For example, if a script is named with the path path/to/script, and it starts with the following line, #!/bin/sh, then the program loader is instructed to run the program /bin/sh, passing path/to/script as the first argument. In Linux, this behavior is the result of both kernel and user-space code.[9]

    • The shebang line is usually ignored by the interpreter, because the "#" character is a comment marker in many scripting languages; some language interpreters that do not use the hash mark to begin comments still may ignore the shebang line in recognition of its purpose.

she-bang with env

The she-bang expects a full path to the interpreter to use so the following syntax would be incorrect:

#!python

Setting a full path like this might work:

#!/usr/local/bin/python

but would be non portable as python might be installed in /bin, /opt/python/bin, or wherever other location.

Using env

#!/usr/bin/env python

is a method allowing a portable way to specify to the OS a full path equivalent to the one where python is first located in the PATH.

I/O redirection

一般情况下,每个 Linux 命令运行时都会打开三个文件:

  • 标准输入文件(stdin):stdin的文件描述符为0,Unix程序默认从stdin读取数据。

  • 标准输出文件(stdout):stdout 的文件描述符为1,Unix程序默认向stdout输出数据。

  • 标准错误文件(stderr):stderr的文件描述符为2,Unix程序会向stderr流中写入错误信息

  • Discard the output

    $ command > /dev/null

    The file /dev/null is a special file that automatically discards all its input.

  • Discard both output of a command and its error output,

    $ command > /dev/null 2>&1
    • a command normally writes its output to STDOUT
    • use standard redirection to redirect STDERR to STDOUT
    • 这里2>&1将标准错误(2)合并到标准输出(1), 而标准输出已经被重定向到了/dev/null, 因此总体效果是,标准错误和输出都被重定向到了/dev/null
  • file descriptor:

    • 0 : STDIN
    • 1: STDOUT
    • 2: STDERR
Sr.No. Command & Description
1 pgm > file
Output of pgm is redirected to file
会覆盖目标文件中原有的数据
2 pgm < file
Program pgm reads its input from file
3 pgm >> file
Output of pgm is appended to file
4 n > file
Output from stream with descriptor n redirected to file
5 n >> file
Output from stream with descriptor n appended to file
6 n >& m
Merges output from stream n with stream m
7 n <& m
Merges input from stream n with stream m
8 << tag
Standard input comes from here through next tag at the start of line
9 |
Takes output from one program, or process, and sends it to another

Here Document

Here Document 目前没有统一的翻译,这里暂译为”嵌入文档“。Here Document 是 Shell 中的一种特殊的重定向方式,它的基本的形式如下:

command << delimiter
document
delimiter

它的作用是将两个 delimiter 之间的内容(document) 作为输入传递给 command

注意:

  • 结尾的delimiter 一定要顶格写,前面不能有任何字符,后面也不能有任何字符,包括空格和 tab 缩进。
  • 开始的delimiter前后的空格会被忽略掉。

下面的例子,通过 wc -l 命令计算 document 的行数:

wc -l << EOF
This is a simple lookup program
for good (and bad) restaurants
in Cape Town.
EOF

也可将 Here Document 用在脚本中,例如:

#!/bin/bash

cat << EOF
This is a simple lookup program
for good (and bad) restaurants
in Cape Town.
EOF

  1. https://lyk-love.cn/2023/04/24/Shell-Script-Searching-Path/↩︎