Shell Script Learning Guide (6) - Input/Output, File and Command Execution


foreword

This blog will complete the introduction of the Shell language. Files are discussed first: how to handle input/output and generate filenames differently. Then comes command substitution, which lets you use the output of a command as an argument on the command line. We then move on to focus on the command line, discussing the various quoting provided by the shell. Finally, it discusses the command execution sequence in depth and introduces the commands built into the shell.

Standard input, standard output, and standard error output

Standard input/output is probably the most fundamental concept in software tool design principles. The idea is that a program should have a data source, a data export, and a place to report problems. They are called standard input, standard output, and standard error output, respectively. A program should not know or care what kind of device is behind its input and output, which could be a disk file, a terminal, a tape drive, a network connection, or even another executing program! A program can expect that when it starts, these standard locations are open and ready for use.

There are many UNIX programs that follow this design philosophy. By default, they read standard input, write standard output, and pass error messages to standard error. As we saw earlier in Chapter 5, such programs we call filters because they "filter" a stream of data, each of which performs some sort of operation on the stream of data, passing it through a pipe to a Next.

read lines using read

readCommands are one of the important ways to pass information to shell programs:

insert image description here

readIt is possible to read all values ​​into multiple variables at once. In this case, $IFSthe characters in will split the data in the input line into separate words. For example:

printf "Enter name, rank, serial number:"
read name rank serno 

The most typical usage is to process /etc/passwdfiles. Its standard format is 7 fields separated by colons: username, encrypted password, numeric user ID, numeric model ID, full name, root directory, and login shell. For example:

[External link picture transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the picture and upload it directly (img-IkSOZd1c-1670490114191)(file://C:\Users\g700382\AppData\Roaming\marktext\images\ 2022-12-05-14-40-23-image.png)]

You can process it line by line with a simple loop /etc/passwd:

while IFS=: read user pass uid gid fullname homedir Shell
do
    ...   #处理每个用户的行
done < /etc/passwd

This loop does not say "When IFS is equal to the colon, then read...", but through the setting of IFS, the readcolon is used as the separator character of the field without affecting the IFS value used in the loop body, that is, It only changes readthe IFS value within the inherited environment. This point was explained earlier.

readExit with a non-zero value when end-of-input-file is encountered . This operation will terminate whilethe loop.

At first glance it might seem /etc/passwdodd to prevent the redirection from ending with the loop body, but it's necessary so that you readsee subsequent lines each time you loop through the loop. If the loop is written like this:

#重定向的不正确使用
while IFS=: read user pass uid gid fullname homedir Shell < /etc/passwd
do
    ...  #处理每个用户的行
done

It will never end! Each time through the loop, the shell will be opened again /etc/passwd, and readonly the first line of the file will be read!

while read .... do ... done < fileThere is an alternative, catusing and looping in a pipeline:

# 较容易读取,不过使用cat会损失一点效率
cat /etc/passwd |
    while IFS=: read user pass uid gid fullname homedir Shell
    do
        ....
    done

There is a common trick: any command can be used to pipe input to read. This technique is especially useful when readused in loops. We have shown the following simple script to copy an entire directory tree:

find /home/tolstoy -type d -print | #寻找所有目录
    sed 's;/home/tolstoy/;/home/lt/;' | # 更改名称;留意使用的是分号定界符
        sed 's/^/mkdir /'    |  #插入mkdir命令
        sh -x  #以Shell的跟踪模式执行

The above example, can actually be done easily, and more naturally from a shell programmer's point of view, using a loop:

find /home/tolstoy -type d -print
    sed 's;/home/tolstoy;/home/lt/;'
       while read newdir
        do
            mkdir $newdir
        done

(we note that this script is not perfect: in particular it does not preserve the ownership and usage permissions of the original directory)

If the input has more words than variables, the last remaining words are all assigned to the last variable. The ideal behavior would be to escape this rule: use reada collocation single variable, and read an entire line of input into that variable.

Long ago, readthe default behavior was to treat the trailing backslash of an input line as an indicator of a continuation line. Such a line causes readthe combination of backslash and newline characters to be discarded, and the next input line to be read continues:

$ printf "Enter name, rank, serial number:" ; read name rank serno
Enter name, rank, serial number: Jones\
> Major \
> 123-45-6789
$ printf "Name: %s, Rank:%s, Serial number:%s\n" $name $rank $serno
Name: Johs, Rank: Major, Serial number: 123-45-6789

Occasionally you'll still want to read an entire line, regardless of which line contains what. -roption to achieve this ( -rthe option is POSIX-specific and not supported by many Bourne shells), and when given -rthe option readdoes not treat a trailing backslash as a special character:

$read -r name rank serno
tolstoy \                      #只提供两个字段
$ echo $name $rank $serno   
tolstoy                        #$serno是空的

About redirection

We have introduced and used the basic I/O redirection operator: <、>、>>,以及|. In this section, we'll look at what other operators are available and introduce important topics about file descriptor handling.

additional redirection operators

Here are the extra operators provided by the shell:

  • Use set -Ccollocation

    • POSIX Shell provides an option to prevent accidental truncation of files: Execute set -Cthe command to turn on the so-called prohibit overwriting option of the shell. When it is turned on, simple >redirection will fail when the target file already exists. >|operator to noclobberdisable options.
  • Provides inline input<<与<<-

    • Using program << delimiter, input data can be provided within the body of the shell script. Such data is called an embedded file. By default, the shell can do variable, command, and arithmetic substitutions within the text of embedded files:
cd /home        #移到根目录的顶端
du -s * |       #产生原始磁盘用量
    sort -nr |  #以数字排序,最高的在第一个
    sed 10q  |  #在前10行之后就停止
    while read amount name
    do
        mail -s "disk usage warning" $name << EOF
Greetings. You are one of the top 10 consumers of disk space
on the system. Your home directory uses $amount disk blocks.

Please clean up unneeded files, as soon as possiable.

Thanks,
Your friendly neighborhood system administrator.
EOF
    done

This example sends an e-mail to the top ten "disk greedy users" on the system, asking them to clean their root directories (in our experience, this kind of information is mostly useless, but doing so will make sysadmins feel better).

The shell does not process the input text if the delimiter is enclosed in leading characters of any kind:

$ i=5                                        #设置变量
$ cat << 'E'OF                               #定界符已被引用
>This is the value of i: $i                  # 尝试变量参照
>Here is a command substitution: $(echo hello, world) #命令替换
>EOF
This is the value of i: $i                   #冗长式地显示文字
Here is a command substitution: $(echo hello, world)

The second form of the embedded file redirector has a minus sign at the end. In this case, all leading tabs (Tabs) are removed from the embedded file and closing delimiters before being passed to the program as input (note: only leading tabs are removed, leading spaces are will not be deleted). Doing so makes the shell script easier to read, let's take a look at the corrected version of the following example notifier:

cd /home        #移到根目录的顶端
du -s * |       #产生原始磁盘用量
    sort -nr |  #以数字排序,最高的在第一个
    sed 10q  |  #在前10行之后就停止
    while read amount name
    do
        mail -s "disk usage warning" $name << -EOF
        Greetings. You are one of the top 10 consumers of disk space
        on the system. Your home directory uses $amount disk blocks.

        Please clean up unneeded files, as soon as possiable.

        Thanks,
        Your friendly neighborhood system administrator.
        EOF
    done
  • Open <>a file for input and output

    • Used program <> filefor read and write operations. The default is to open file on standard input. In general, <files are opened in read-only mode and >files are opened in write-only mode. <>The operator opens the given file in both read and write modes. It's up to you programto determine and make good use of; in fact, using this operator doesn't require much support.

file descriptor handling

Internally, UNIX uses a small integer number, called a file descriptor, to represent the open files of each process. Numbers start at zero and go up to a system-defined limit on the number of open files. Traditionally, Shell allows you to directly handle up to 10 open files: file descriptors from 0 to 9 (POSIX standard will be able to handle file descriptors greater than 9, left to self-definition. Yes, but not bash) ksh.

File descriptors 0, 1, and 2 correspond to standard input, standard output, and standard error output, respectively. As mentioned earlier, every program starts with these file descriptors attached to a terminal (whether it is a real terminal or a virtual terminal, such as an X window). By far the most common operation is to change the location of one of the three file descriptors, but other changes may be handled as well. First things first: pipe the program's output to one file, and its errors without messages to another:

make 1> results 2> ERRS

The above command is makepassing standard output resultsto and standard error output to ERRS(make doesn't know the difference: it doesn't know or care, and it doesn't send output or error messages to the terminal). It is useful to capture error messages in a separate file, so that you can refer to them later with a paging program, or use an editor to fix problems. Otherwise, a large number of output error messages will quickly scroll across the screen, and it will be difficult for you to find the information you need. Another different way is more neat, just discard the error message;

make 1> results 2> /dev/null

1>resultsThe 1 in is actually unnecessary, the default file descriptor for output redirection is standard output: that is, file descriptor 1. The next example sends output and error messages to the same file:

make > results 2>&1

The redirection > resultsmakes file descriptor 1 (standard output) the file results, and the following redirection 2>&1has two parts. 2>Redirect file descriptor 2, which is standard error output. Instead,&1 shell syntax: wherever file descriptor 1 is. In this example, file descriptor 1 is resultsthe file, so that's where file descriptor 2 will be attached. One thing to pay special attention to is: on the command line, these 4 characters 2>&1must be connected together without any spaces in between.

Here, the order is very important: when the shell processes redirection, it goes from left to right. Consider this example:

make 2>&1 >results

For the above command, Shell will first send the standard error message to file descriptor 1, which is still a terminal at this time, and then file descriptor 1 (standard output) is changed results. Further, the shell handles pipes before file descriptor redirection, allowing us to pipe both stdout and stderr to the same pipe:

make 2>&1 | ...

Finally, there are execcommands that can be used to change the I/O settings of the shell itself. When used, if there is only I/O redirection without any parameters, execit will change the shell's file descriptor:

exec 2> /tmp/$0.log    #重定向Shell本身的标准错误输出
exec 3> /some/file     #打开新文件描述符3
...
read name runk serno <3  #从该文件读取

insert image description here

With parameters, execit can also play another role, that is, execute the specified program under the current Shell. In other words, the shell starts the new program in its current process. For example, when you want to use the shell for option processing but still have most of it done by some other program, you can do it this way:

while [ $# -gt 1 ]    #循环遍历参数
do
    case $1 in
    -f)               # code for -f here
        ;;
    -q)               # code for -q here
        ;;
    ...
    *)    break ;;    #没有选项,中断循环
    esac
    shift              #移到下一个参数
done
exec real-app -q "$qargs" -f "$fargs" "$@"  #执行程序
echo real-app failed, get help! 1>&2        #紧急信息

When using this method, execit is a one-way operation. That is to say: there is no way control will go back to the script. The only exception is when the new procedure cannot be called, in which case we'd like to have "emergency" code that can display a message and then do other possible cleanup.

A complete introduction to printf

We introduced printfthe command earlier, and we will introduce it in full below.

printfThe full syntax of the command has two parts:

Printf format-string [arguments...]

The first part is a descriptor specification string, which is best provided as a string constant enclosed in quotes. The second part is a parameter list, such as a list of strings or variable values, which needs to correspond to the format specification. Format strings combine text to be output literally, using specifications that describe how to printfformat a sequence of arguments on the command line. General characters are output literally. Escape sequences are interpreted (similar to echo) and output as the corresponding characters. Format specifiers %begin with a character and end with one of the defined alphabets, and are used to control the output of the corresponding argument that follows. printfSee the table below for escape sequences:

insert image description here

escape sequence for printf

sequence illustrate
\a Warning character, usually ASCII BEL character
\b step back
\c Suppress (do not display) any trailing newline characters in the output; furthermore, any characters left in the argument, any subsequent arguments, and any characters left in the format string are ignored
\f change page
\n new line
\r carriage return
\t horizontal tab
\v vertical tab
\\ a literal backslash character
\ddd A character representing a 1- to 3-digit octal value. only valid in the format string
\0ddd An octal value character representing 1 to 3 digits

printfThe handling of escape sequences can be confusing. By default, escape sequences are treated specially only in format strings, that is, escape sequences appearing in argument strings are not interpreted:

$ printf "a string, no processing: <%s>\n" "A\nB"
a string, no processing: <A\nB>
#当你使用%b格式指示符时,printf会解释参数字符串里的转义序列
$ printf "a string, with processing: <%b>\n" "A\nB"
a string, with processing:<A
B>

%bMost escape sequences are treated the same whether they are in the format string or in the argument string that is printed. In any case, \cand are only valid when used \0dddtogether , and are only interpreted in format strings.%b\ddd

It should now be roughly concluded that format specifiers printfprovide power and flexibility. Format specification inter-letter table below

project illustrate
%b The corresponding argument is treated as a string containing escape sequences to be processed (compare table above)
%c ASCII characters. Display the first character of the corresponding parameter
%d,%i decimal integer
%e floating point format
%E floating point format
%f floating point format
%g %e or %f conversion, whichever is shorter, removes trailing zeros
%G %E or %f conversion, whichever is shorter, removes trailing zeros
%o unsigned octal value
%s string
%u unsigned hexadecimal value
%x Unsigned hexadecimal value. Use a to f for 10 to 15
%X Unsigned hexadecimal value. Use A to F for 10 to 15
%% Literal %

According to the POSIX standard: the floating point format %e、%E、%f、%g与%Gis not required to be supported. This is because awkfloating-point arithmetic is supported and has its own printfstatement. In this way, when the floating-point value needs to be formatted and printed in the Shell program, it can be awkrealized with a small program. printfHowever, commands built into bash, ksh93, and zsh all support floating-point formats.

printfcommand can be used to specify the width of the output field and perform alignment operations. To do this, the following %format expression can take three optional modifiers and a preceding format indicator:

%flags width.precision format-specifier

The output field widthis a numeric value. When specifying the field width, the content of the field is aligned to the right by default. If you want the text to be aligned to the left, you must specify the - flag. This way: "%-20s" will output a left-aligned string in a field with a width of 20 characters. If the string is less than 20 characters, the subsection will be filled with blanks. In the example below, |it is output to indicate the actual width of the field. The first example aligns text to the right:

$ printf "|%10s|\n" hello
|    hello|

The precision modifier is optional. For decimal or floating-point numbers, it controls how many digits the number appears in the result. For strings, this controls the maximum number of characters in the string that will be printed. The exact meaning varies with the format specifier.

The meaning of precision

convert precision meaning
%and and The minimum number of digits to print. When the number of digits of the value is less than this number, zeros will be added after the decimal point, and the default precision is 6. The precision of 0 means that no decimal point is displayed
%f the number of digits to the right of the decimal point
%g,%G Maximum number of significant digits
%s maximum number of characters to print

Here are a few precision examples:

$ printf "%.5d\n" 15
00015
$ printf "%.10s\n" "a very long string"
a very lon
$ printf "%.2f\n" 123.4567
123.45

C library printf()functions allow width and precision to be specified dynamically via additional values ​​in the parameter list.

The POSIX standard does not support this feature, instead it recommends that you use shell variable values ​​in the format string

Examples are as follows:

$ width=5 prec=6 myvar=42.123456
$ printf "|%$(width).$(prec)G|\n" $myvar  #POSIX
|42.1235|
$ printf "|%*。*G|\n" 5 6 $myvar  #ksh93 与bash
|42.1235|

One last thing to cover: the usage of one or more flags before the field width and precision. We have already covered the use of -flags to align strings to the left. Wanheng's logo list is as follows:

character significance
- Align the formatted value in character to the left
Blank (space) Precede positive values ​​with a space and negative values ​​with a minus sign
+ Always place a plus or minus sign before a value, even if it is positive
# One of the following forms is chosen: %o has a leading O; %g and %G have no trailing zero.
0 Pad the output with zeros, not blanks. This only happens if the field width is larger than after conversion. In C, this flag applies to all output formats, even non-numeric values. For printfcommands, it only applies to numeric formats

Another example:

$ printf "|%-10s| |%10s|\n" hello world #字符串向左、向右看齐
|helo    | |    world|
$ printf "|% d| |% d|\n" 15 -15 #空白标志
| 15| |-15|
$ printf "%+d %+d\n"    15 -15 # +标志
+15 -15
$ printf "%x %#x" 15 -15 # #标志
f 0xf
$ printf "%05d\n" 15  # 0标志
00015

For conversion specifiers %b、%c与%s, the corresponding arguments are treated as strings. Otherwise, they are interpreted as C numeric constants (leading 0s for octal, and leading 0x and 0X for hexadecimal). Further, if the first character of the parameter is a single quote or double quote, the corresponding value is the ASCII value of the second character of the string:

$ printf "%s is %d\n" a "'a"
a is 97

当参数多于指示符时,格式指示符会根据需要再利用。这种做法在参数列表长度未知时很方便,例如来自通配符表达式。如果留在格式字符串里剩下的指示符比参数多时,如果是数值转换,则遗漏的值会被看成是零,如果是字符串转换,则被视为空字符串(虽然可以这么用,但比较好的方式应该是确认你提供的参数数目,与格式字符串所预期的数目是一样的)。如果printf无法进行格式的转换,它便会返回一个非零的退出状态。

波浪号展开与通配符

Shell有两种与文件名为相关的展开。第一个是波浪号展开,另一个则有很多叫法,有人称之为通配符展开式,有人则称之为全局展开或路径展开

波浪号展开

如果命令行字符串的第一个字符为波浪号(~),或者变量指定(例如PATH或ADPATH变量)的值里任何未被引号括起来的冒号之后的第一个字符为波浪号(~)时,Shell便会执行波浪号展开。

波浪号展开的目的,是要将用户根据目录的符号型表达方式,改为实际的目录路径。可以采用直接或间接的方式指定执行此程序的用户,如未明白指定,则为当前的用户:

$ vi ~/.profile    #与vi $HOME/.profile相同
$ vi ~tolstoy/.profile #编辑用户tolstoy的.profile文件

以第一个例子来看,Shell将~换成$HOME,也就是当前用户的根目录。第二个例子,则是Shell在系统的密码数据库里,寻找用户tolstoy,再将~tolstoy置换为tolstoy的根目录。

使用波浪号展开有两个好处。第一,它是一种简洁的概念表达方式,让查阅Shell脚本的人更清楚脚本在做的事。第二,它可以避免在程序里把路径直接编码。先看这个脚本片段:

printf "Enter username: "  #显示提示信息
read user                  #读取指定的用户名
vi /home/$user/.profile    #编辑该用户的.profile文件

前面的程序假设所有用户的根目录都在/home之下。如果这有任何变动(例如,用户子目录根据部门存放在部门目录的子目录下),那么这个脚本就得重写。但如果使用波浪号展开,就能避免重写的情况:

printf "Enter username: "
read user
vi ~$user/.profile
...

这样一来,无论用户根目录在哪里,程序都可以正常工作。

使用通配符

寻找文件名里的特殊字符,也是Shell提供的服务之一。当它找到这类字符时,会将它们视为要匹配的模式,也就是,一组文件的规格,它们的文件名称都匹配于某个给定的模式。Shell会将命令行上的模式,置换为符合模式的一组排序过的文件名。

如果接触过MS-DOS下的简单命令行环境,你可能会觉得*.*这样的通配符很熟悉,它指的是当前目录下所有文件名。UNIX Shell的通配符也类似,只不过功能更强大。基本的通配符见下表。

通配符 匹配
任何的单一字符
* 任何的字符字符串
[set] 任何在set里的字符
[!set] 任何不在set里的字符

通配符匹配于任何的单一字符,所以如果你的目录里含有whizprog.c、whizprog.log与whizprog.o这三个文件,与表达式whizprog.?匹配为whizprog.c与whizprog.o,但whizprog.log则不匹配。

星号(*)是一个功能强大而且广为使用的通配符;它匹配于任何字符组成的字符串。表达式whizprog.*符合前面列出的所有三个文件;网页设计人员也可以使用*.html表达式匹配他们的输入文件。

剩下的通配符就是set结构了。set是一组字符列表(例如abc)、一段内含的范围(例如a-z),或者是这两者的结合。如果希望破折号(dash)也是列表的一部分,只要把它放在第一个或最后一个就可以了。参考下表:

表达式 匹配的单一字符
[abc] a、b或c
[.,;] 局点、逗点或分号
[-_] 破折号或下划线
[a-c] a、b或c
[a-z] 任何一个小写字母
[!0-9] 任何一个非数字字符
[0-9!] 任何一个数字或惊叹号
[a-zA-Z] 任何一个小写或大写的字母
[a-zA-Z0-9_-] 任何一个字母、任何一个数字、下划线或破折号

范围表示法固然方便,但你不应该对包含在范围内的字符有太多的假设。比较安全的方式是:分别指定所有大写字母、小写字母、数字,或任意的子范围。不要想在标点符号字符上指定范围,或者在混用字母大小写上使用,想[a-Z]与[A-Z]这样的用法,都不保证一定能确切地匹配出包括所有想要的字母,而没有其他不想要的字符。更大的问题在于:这样的范围在不同类型的计算机之间无法提供完全的可移植性。

另一个问题是:现行系统支持各种不同的系统语言环境,用来描述本地字符集的工作方式。很多国家的默认local字符集与纯粹ASCII的字符集是不同的。为解决这些问题,POSIX标准提出了方括号表达式,用来表示字母、数字、标点符号及其他类型的字符,并且具有可移植性。

习惯上,当执行通配符展开时,UNIX Shell会忽略文件名开头为一个点号的文件。像这样的“点号文件”通常用做程序配置文件或启动文件。像是Shell的$HOME/.profile、ex/vi编辑器的$HOME/.exrc,以及bash与gdb使用的GNU的readline程序库的$HOME/.inputrc.

要是看到这类文件,需在模式前面明确地提供一个点号,例如:

echo .*    #显示隐藏文件

你可以使用-a(显示全部)选项,让ls列出隐藏文件:

insert image description here

命令替换

命令替换是指Shell执行命令并将命令替换部分替换为执行该命令后的结果。这听起来雨点饶舌,不过实际上相当简单。

命令替换的形式有两种。第一种是使用反引号——或称重音符(``)的方式,将要执行的命令框起来:

for i in `cd /old/code/dir; echo *.c`  #产生/old/code/dir下的文件列表
do
    diff -c /old/code/dir/$i $i | more #循环处理,在分页程序下比较旧版与新版的异同
done

这个Shell一开始执行cd /old/code/dir; echo *.c产生的输出结果(文件列表)接着会成为for循环里所使用的列表。

反引号形式长久以来一直是供命令替换使用的方法,而且POSIX也支持它,因此许多已存在的Shell脚本都使用它。不过,所有简单的简答的用法很快变成复杂的,特别是内嵌的命令替换及使用双引号时,都需要小心地转义反斜杠字符:

$ echo outer `echo inner1 \`echo inner2\` inner1` outer
outer innner1 inner2 inner1 outer

这个例子举得有点牵强,不过正说明了必须使用反引号的原因。命令执行顺序如下:

  1. 执行echo innner2,其输出为单词inner2会放置到下一个要被执行的命令中。

  2. 执行echo innner1 inner2 inner1,其输出单词会放置到下一个要执行的命令中。

  3. 最后,执行echo outer inner1 innner2 innner1 outer

使用双引号,情况更糟:

$ echo "outer + `echo innner -\`echo \"nested quote\"here\`-innner`+ouuter"
outer + inner - nested quote here- inner+ outer

为了更清楚明白,我们使用负号括住内部的命令替换,而使用正号框柱外部的命令替换。简而言之,就是更为混乱。

由于使用嵌套的命令替换,有没有引号,很快地就变得很难阅读,所以POSIX Shell采用Korn Shell里的一个功能。不用反引号的用法,改为将命令括在$(...)里。因为这种架构使用不同的开始定界符与结束定界符,所以看起来容易多了。以先前的例子来看,使用新语法重写如下:

$ echo outer $(echo inner1 $(echo inner2) inner1) outer
outer inner1 inner2 inner1 outer
$ echo "outer +$(echo inner -$(echo "nested quote" here)- inner)+ outer"
outer +inner -nested quote here- inner+ outer

这样是不是好看多了?不过特别留意的是:内嵌的双引号不在需要转义。这种风格已广泛建议用在新的开发上,我们也主要使用这种方法。

这边要看的是先前介绍过的,使用for循环比较不同的两个目录下的文件版本,以新语法重写如下:

for i in $(cd /old/code/dir ; echo *.c)
do
    diff -c /old/code/dir/$i  $i
done | more

这里的不同之处在于使用$(...)命令替换,以及将整个循环的输出,通过管道送到more屏幕分页程序。

为head命令使用sed

前面介绍使用过sedhead命令来显示文件的前n行。真实的head命令可以加上选项,以指定要显示多少行,例如head -n 10 /etc/passwd。传统的POSIX版本之前的head可指明行数作为选项,且许多UNIX的长期用户也习惯于使用此法执行head

使用命令替换与sed,我们对Shell脚本稍作修改,使其与原始head版本的工作方式相同。

# head --打印前n行
#
# 语法: head -N file
count=$(echo $1 | sed 's/^-//' | sed 's/$/q/')
shift
sed $count "$@"

当我们以head -10 foo.xml调用这个脚本时,sed最终是以sed 10qfoo.xml被引用。

创建邮件列表

不同的UNIX Shell的新版本不断地出现,而且分散在各站点的用户可以从/etc/Shells所列的Shell中选择自己的登录Shell。这样,如果以email通知用户有新版本的Shell更新且已安装的功能,这对系统管理而言会是很不错的事。

为了实现这个目的,我们必须先以登录Shell来识别用户,且产生邮件列表供安装程序用来公告新的Shell版本。由于每封通知信息内容都不尽相同,我们也不是要建立一个直接传送邮件的脚本,只是建立一个可用来寄送邮件的地址列表。邮件列表格式会因邮件用户端程序而有所不同,所以我们可以做一个合理的假设:最后完成的,只是一个以逗点分割电子邮件地址的列表,一行一个或多个地址,而最后一个地址之后是否有逗点则不重要。

这种情况下,较合理的方式应该是听过密码文件处理,为每个登录Shell建立一个输出文件,文件中每一行的用户名称都以逗点结束。

脚本本身结合了变量与命令替换,read命令以及while循环,整个脚本执行的代码不到10行!

#! /bin/sh
# passwd-to-mailing-list
#
# 产生使用特定 Shell 的所有用户邮寄列表
#
# 语法:
# passwd-to-mailing-list < /etc/passwd
# ypcat passwd | passwd-to-mailing-list
# niscat passwd.org_dir | passwd-to-mailing-list


# 此操作或许有些过度小心
rm -f /tmp/*.mailing-list
# 从标准输入读取:
while IFS=: read user passwd uid gid name home Shell
do
    Shell=$(Shell:-/bin/sh) #空的 Shell 字段意思指 /bin/sh
    file="/tmp/$(echo $Shell | sed -e 's;^/;;' -e 's;/;-;-').mailing-list"
    echo $user, >>$file
done

每次读取密码文件的记录时,程序都会根据Shell的文件名产生文件名。sed命令会删除前置/字符,并将后续的每个/改成-连字号。这段脚本会建立/tmp/bin-bash.mailing-list。这样形式的文件名。每个用户的名称与结尾的逗点都通过>>附加到特定的文件中。执行这个脚本后,会得到以下结果:

$ cat /tmp/bin-bash.mailing-list
dorothy,
ben,
jhancock,
tj,

我们可以让这个建立邮件列表的程序更广泛地应用。例如,如果系统的进程统计是打开的,要为系统里的每个程序做一份邮件列表就很容易了,只要从进程统计记录中取出程序名称和执行过程的用户姓名即可。注意,访问统计文件必须拥有root权限。每个厂商提供的统计软件都不一样,不过它们累积的数据的种类都差不多,所以只要稍作微调即可。GNU的统计摘要工具:sa可产生类似下面这样的报告。

# sa -u
...
jones    0.01 cpu    377k    mem     0 io gcc
...

也就是说,我们有一个以空白隔开的字段,第一个字段为用户名称而且最后一个字段为程序名称。这让我们可以简单地过滤该输出,使其看起来像是密码文件数据类型,再将其通过管道传递给我们的邮件列表程序来处理:

sa -u | awk '{ print $1 "::::::" $8}' | sort -u | passwd-to-mailing-list

(sort命令是用来排序数据; -u选项则删除重复的行。)这个UNIX过滤程序与管道以及简单的数据标记,最漂亮的地方在于简洁。我们不必编写新的邮件列表产生程序来处理统计数据:只需要一个简单的awk步骤以及一个sort,就可以让数据看起来就像我们可以处理的那样!

简易数学:expr

expr命令是UNIX命令里少数几个设计的不是那么严谨又很难用的一个。虽然经过POSIX标准化,但我们非常不希望你在新程序里使用它,因为还有更多其他程序与工具做的比它更好。在Shell脚本的编写上,expr主要用于Shell的算术运算,所以我们把重点放在这就好。如果你真有那么强的求知欲,可以参考expr(1)手册页了解更详尽的使用方法。

expr的语法很麻烦:运算数与运算符必须是单个的命令行参数;因此我们建议你在这里大量使用空格间隔他们。很多expr的运算符同时也是Shell的meta字符,所以必须谨慎使用引号。

expr被设置用来命令替换之内。这样,它会通过打印的方式把值返回到标准输出,而并非通过使用退出码(也就是Shell内的$?

expr运算符

insert image description here

在新的代码里,你可以使用test或$((...))进行这里的所有运算。正则表达式的匹配于提取,也可搭配sed或是Shell的case语句来完成。

这里的一个简单算术运算的例子。在真实的脚本里,循环体会做一些较有意义的操作,而不只是把循环变量的值显示出来:

$ i=1        #初始化计数器
$ while [ "$i" -le 5 ] #循环测试
>do
>    echo i is $i
>    i = `expr $i + 1`
>done
i is 1  
i is 2  
i is 3  
i is 4  
i is 5
$ echo $i
6  

这类算术运算,已经给出了你可能遇到的expr的使用方式的99%。我们故意在这里使用test(别名用法为[...])以及反引号的命令替换,因为这是expr的传统用法。在新的代码里,使用Shell的内建算术替换应该会更好:

$ i=1        #初始化计数器
$ while [ "$i" -le 5 ] #循环测试
>do
>    echo i is $i
>    i = $((i+1))
>done
i is 1  
i is 2  
i is 3  
i is 4  
i is 5
$ echo $i
6  

无论expr的价值如何,它支持32位的算术运算,也支持64位的算术运算——在很多系统上都可以,因此,几乎不会有计算器溢出(overflow)的问题。

引用

引用时用来防止Shell将某些你想要的东西解释成不同的意义。举例来说,如果你要命令接受含有meta字符的参数,如*?,就必须将这个meta字符用引号引用起来。或更典型的情况是:你希望将某些可能被Shell视为个别参数的东西保持为单个参数,这时你就必须将其引用。这里是三种引用的方式:

  • 反斜杠转义

    • 字符前置反斜杠(\),用来告知Shell该字符即为其字面上的意义。这是引用单一字符最简单的方式:
$ echo here is a real star: \* and a real question mark: \?
here is a real star: * and a real question mark: ?
  • 单引号

    • 单引号('...')强制Shell将一对引号之间的所有字符都看做其字面上的意义。Shell脚本会删除这两个引号,只单独留下被括起来的完整文字内容:
$ echo 'here are some metacharacters: *? [abc] ` $ \'
here are some metacharacters: * ? [abc] ` $ \

不可以在一个单引号引用的字符串里再做内嵌一个单引号。即便是反斜杠,在单引号里也没有特殊意义(某些系统里,像echo 'A\tB这样的命令看起来像是Shell特别地处理反斜杠,其实不然,这是echo命令本身有特殊的处理方式)。

如需混用单引号与双引号,你可以小心地使用反斜杠转义以及不同引号字符串的连接做到:

$ echo 'He said, "How'\''s tricks?"'
He said, "How's tricks?"

不管你怎么处理,这种结合方式永远都是很那阅读的。

  • 双引号

    • 双引号("...")就像单引号一样,将括起来的文字视为单一字符串。只不过,双引号会确切地处理括起来文字中转义字符和变量、算术、命令和替换:
$ x="I am x"
$ echo "\$x is \"$x\".Here is some output: '$(echo Hello World)'"
$x is "I am x". Here is some output: 'Hello World'

在双引号里,字符$、"、与\,如需用到字面上的意义,都必须前置\。任何其他字符前面的反斜杠是不带有特殊意义的。序列\-newline会完全地被删除,就好像是用在脚本的正文中一样。

请注意,如范例所示,单引号被括在双引号里时就无特殊意义了,它们不比成对,也无须转义。

一般来说,使用单引号的时机是你希望完全不处理的地方。否则,当你希望将多个单词视为单一字符串,但又需要Shell为你做一些事情的时候,请使用双引号,例如,将一个变量值与另一个变量值连接在一起,你就可以这么做:

oldvar="$oldvar $newvar"  #将newvar的值附加到oldvar变量

执行顺序与eval

我们曾经提到过的各类展开与替换都以定义好的次序完成。POSIX标准更提供了很多琐碎的细节。在这里,我们站在Shell程序设计人员的层面来看这些必须了解的东西。这里的解释,省略了许多小细节:例如复合命令的中间与结尾、特殊字符等。

Shell从标准输入或脚本中读取的每一行称为管道;它包含了一个或多个命令,这些命令被零或多个管道字符(|)隔开。事实上还有很多特殊符号可用来分隔单个的命令;分号(;)、管道(|)、&、逻辑AND(&&),还有逻辑OR(||)。对于每一个读取的管道,Shell都会将命令分割,为管道设置I/O,并且对每一个命令一次执行下面操作:

  1. 将命令分割成token,是以固定的一组meta字符分隔,有空格、制表符、换行字符、;、(,)、<、>、|与&。token的种类包括单词、关键字、输出入重定向器,以及分号。这是微妙的,但是变量、命令还有算术替换,都可以在Shell执行token认定时候被执行。

  2. 检查每个命令的第一个token,看看是否它是不带引号或反斜杠的关键字。如果它是一个开放的关键字(if与其他控制结构的开始符号,如{或(,则这个命令其实是一个复合命令)。Shell为复合命令进行内部的设置,读取下一条命令,并再次启动进程。如果关键字非符合命令的开始符号(例如,它是控制结构的中间部分,像then、else或do,或是结尾部分,例如fi、done或逻辑运算符),则Shell会发出语法错误的信号。

  3. 将每个命令的第一个单词与别名列表对照检查。如果匹配,它便代替别名的定义,并回到步骤1;否则,进行步骤4(别名是给交互式Shell使用,因此我们在这里不谈)。回到步骤1,允许让关键字的别名被定义:例如alias aslongas=while or alias procedure=function.注意,Shell不会执行递归的别名展开:反而当别名展开为相同命令时它会知道,并停止潜在的递归操作。可以通过引用要被保护的单词的任何部分而禁止别名展开。

  4. 如果波浪号(~)字符出现在单词的开头,则将波浪号替换成用户的根目录($HOME)。将~user替换成user的根目录。波浪号替换会发生在下面的位置

  5. 在命令行里,作为单词的第一个未引用字符

  6. 在变量赋值中=之后以及变量赋值中的任何:之后

  7. 形式${variable op word}的变量替换里的word部分。

  8. 将任何开头为$符号的表达式、执行参数(变量)替换。

  9. 将任何形式为$(string),执行命令替换。

  10. 执行形式$((string))的算术表达式。

  11. 从参数、命令与算术替换中取出结果行的部分,再一次将它们切分为单词。这次它使用$IFS里的字符作为定界符,而不是使用步骤1的meta字符。通常,在IFS里连续多个重复的输入字符是作为单一定界符,这是你所期待的。这只有对空白字符而言是真的。对于非空白字符,则不是这样的。举例来说,当读取以冒号分割字段的/etc/passwd文件,连续冒号锁定接的是一个空字段。

  12. 对于*、?以及一对[...]的任何出现次数,都执行文件名生成的操作,也就是通配符展开。

  13. 使用第一次单词作为一个命令;也就是,先作为一个特殊的内建命令,接着是作为函数,然后作为一般的内建命令,以及最后作为查找$PATH找到的第一个文件。

  14. 在完成I/O重定向与其他同类型事项之后,执行命令。

如下图所示,引用可用来避免执行程序的不同部分。eval命令可以让你再经过一次这一流程。执行两次命令行的处理,看来似乎有点奇怪,不过这却是相当好用的功能:它让你编写一个脚本,可在任务中建立命令字符串,再将它们传递给Shell执行。这么一来,你可以让脚本聪明地修改它们执行时的行为。

insert image description here

这个步骤看起来有点复杂;当命令行被处理时,每个步骤都是在Shell的内存里发生的;Shell不会真地把每个步骤的发生显示给你看。所以,你可以假象这是我们偷窥Shell内存里的情况,从而知道每个阶段的命令行是如何被准换的。我们从这个例子开始说:

$ mkdir /tmp/x            #创建临时性目录
$ cd /tmp/x               #切换到该目录
$ touch f1 f2             #建立文件
$ f=f y="a b"             #赋值两个变量
$ echo ~+/${f}[12] $y $(echo cmd subst) $((3+2)) > out #忙碌的命令

上述的执行步骤概要如下:

  1. 命令一开始会根据Shell语法而分割token。最重要的一点是:I/O重定向>out在这里是被识别的,并存储供稍后使用。流程继续处理下面这行,其中每个token的范围显示于命令下方的行上:insert image description here

  2. 检查第一个单词(echo)是否为关键字,例如if或for。在这里不是,所以命令行不变继续处理。

  3. 检查第一个单词(依然是echo)是否为别名。在这里并不是,所以命令行不变,继续往下处理。

  4. 扫描所有单词是否需要波浪号展开。在本例中,~+为ksh93与bash的扩展,等通过$PWD,也就是当前目录。token2将被修改,处理继续如下
    insert image description here

  5. 下一步是变量展开:token2与3都被修改。这会产生

insert image description here

  1. 再来要处理的是命令替换。注意,这里可递归引用列表里的所有步骤!在此例中,因为我们试图让所有的东西容易理解,因此命令替换修改了token4,结果如下:
    insert image description here

  2. 现在执行算术替换。修改的是token5,结果是:
    insert image description here

  3. 前面所有的展开产生的结果,都将再一次被扫描,看看是否有$IFS字符。如果有,则它们是作为分隔符,产生额外的单词。例如,两个字符$y原来是组成一个单词,但是展开式"a-空格-b",在此阶段被切分为两个单词;a与b。相同方式也应用与命令替换$(echo cmd subset)的结果上。先前的token 3变成了token3与4。先前的token4则变成的token5与6,结果为:
    insert image description here

  4. 最后的替换阶段是通配符展开。token2变成了token2与3,结果如下:
    insert image description here

  5. 这时,Shell已经准备好执行最后的命令了。它会去寻找echo。正好ksh93与bash里的echo都已内建到Shell中。

  6. Shell实际执行命令。首先执行>out的I/O重定向,再调用内部的echo版本,显示最后的参数。

最后的结果:

$ cat out
/tmp/x/f1 /tmp/x/f2 a b cmd subst 5

eval语句

eval语句是在告知Shell取出eval的参数,并再次执行它们一次,使它们经过整个命令行的处理步骤。这里有个例子可以让你了解eval究竟是什么。

eval ls会传递字符串ls给Shell执行,所以Shell显示当前目录下的文件列表。这个例子太简单了:字符串ls上没有东西需要被送出让命令处理两个步骤。所以我们来看这个:

listpage="ls | more"
$ listpage

为什么Shell会把|more看作ls的参数,而不是直接产生一页页的文件列表呢?这是由于Shell执行变量时,管道字符出现在步骤5,也就是在它确实寻找管道字符后(在步骤1)。变量的展开一直要到步骤8才进行解析。结果,Shell把|more看做ls的参数,使得ls会试图在当前目录下寻找名为|more的文件!

现在,想想eval $listpage吧。在Shell到达最后一步时,会执行带有ls、|与more参数的eval。这会让Shell回到步骤1,具有一行包括了这个参数的命令。在步骤1发现|后,将该行分割为两个命令:lsmore。每个要被处理的命令都以一般方式执行,最后的结果是在当前目录下已分页的文件列表。

subShell与代码块

还有两个其他的结构,有时也很有用:subShell与代码块

subShell是一群被括在圆括号里的命令,这些命令会在另外的进程中执行。当你需要让一小组的命令在不同的目录下执行时,这种方法可以让你不必修改主脚本的目录,直接处理这种情况。例如,下面的管道是将某个目录树复制到另一个地方,在原始的V7 UNIX 的手册页里可以看到此例:

tar -cf - . | (cd /newdir; tar -xpf -)

左边的tar命令会产生当前目录的tar打包文件,将它传送给标准输出。这份打包文件按会通过管道传递给右边subShell里的命令。开头的cd命令会切换到新目录,也就是让打包文件在此目录下解开。然后,右边的tar将从打包文件中解开文件。请注意,执行此管道的Shell并未更改它的目录。

代码块概念上与subShell雷同,只不过它不会建立新进程。代码块里的命令以花括号({})括起来,且对主脚本的状态会造成影响(例如它的当前目录)。一般来说,花括号被视为Shell关键字:意即它们只有出现在命令的第一个符号时会被识别。实际上:这表示你必须将结束花括号放置在换行字符或分号之后,例如:

cd /some/directory || {
    
                                     #代码块开始
    echo could not change to /some/directory! >&2       #怎么了
    echo you lose! >&2                                 #挖苦信息
    exit 1                                              #终止整个脚本
}                                                       #代码块结束

I/O重定向也可套用到subShell(如先前的两个tar例子)与代码块里。在该情况下,所有的命令会从重定向来源读取它们的输入或传送它们的输出。下面简单说明了subShell与代码块之间的差异:
insert image description here

要用subShell还是代码块需要根据个人的喜好而定。主要差异在于代码块会与主脚本共享状态。这么一来,cd命令会对主脚本造成影响,也会对变量赋值产生影响。特别是,代码块里的exit会终止整个脚本。因此,如果你希望将一组命令括起来执行,而不影响主脚本,则应该使用subShell。否则,请使用代码块。

内建命令

Shell有为数不少的内建命令。指的是由Shell本身执行命令,而并非在另外的进程中执行外部程序。POSIX更进一步将它们区分为“特殊”内建命令以及“一般”内建命令。特殊内建命令以+标示。大部分这里列出的一般内建命令都必须内建于Shell中,以维持Shell正常运行(例如,read)。其他内建于Shell内的命令,则只是为了效率(例true与false)。在此标准下,也为了更有效率,而允许内建其他的命令,不过所有的一般内建命令都必须能够以单个的程序被访问,也就是可以被其他二进制程序直接执行。像test内建命令就是为了让Shell的执行更有效率。

命令 摘要
:(冒号) 不做任何事(只做参数的展开)
.(点号) 读取我呢间并与当前Shell中执行它的内容
alias(别名) 设置命令或命令行的捷径(交互式使用)
bg 将工作置于后台(交互式使用)
break 从for、while或until循环中退出
cd 改变工作目录
command 找出内建于外部命令;寻找内建命令而非同名函数
continue 跳到下一个for、while或until循环重复
eval 将参数当成命令行来处理
exec 给定的程序取代Shell或为Shell改变I/O
exit 退出Shell
export 建立环境变量
false 什么事也不做,指不成功的状态
fc 与命令历史一起运行(交互式使用)
fg 将后台工作置于前台(交互式使用)
getopts 处理命令行的选项
jobs 列出后台工作(交互式使用)
kill 传送信号
newgrp 以新的group ID启动新的Shell(已过时)
pwd 打印当前目录
read 从标准输入中读取一行
readonly 让变量为只读模式(无法指定的)
return 从包围函数中返回
set 设置选项或是位置参数
shift 移位命令行参数
times 针对Shell与其子代Shell,显示用户与系统CPU时间的累计
trap 设置信号捕捉程序
ture 什么事也不做,表示成功状态
umask 设置/显示文件权限掩码
unalias 删除别名定义(交互式使用)
unset 删除变量或函数的定义
wait 等待后台工作完成

注意:bash的source命令(来自于BSD C Shell)是等同于点号命令。

特殊内建命令与一般内建命令的差别在于Shell查找要执行的命令时。命令查找次序是先找特殊内建命令,再找Shell函数,接下来才是一般内建命令,最后则为$PATH内所列目录下找到的外部命令。这种查找顺序让定义Shell函数以扩展或覆盖一般Shell内建命令成为可能。

这一功能最常用于交互式Shell中。举例来说,当你希望Shell的提示号能包含当前目录路径的最后一个组成部分。最简单的实现方式,就是在每次你改变目录时,都让Shell改变PS1.你可以写一个自己专用的函数,如下所示:

# chdir ---在改变目录时也更新PS1的个人函数
chdir(){
    
    
    cd "$@"    #实际更改目录
    x=$(pwd)    #取得当前目录的名称,传给变量x
    PS1="${x##*/}\$"
}

这么做会有一个问题:你必须在Shell下输入chdir而不是cd,如果你突然忘了而输入cd你会在新的目录上,提示号就不会改变了。基于这个原因,你可以写一个名为cd的函数,然后Shell就会先找到你的函数,因为cd是一般内建命令:

#cd ---改变目录时更新 PS1的个人版本
#    (无法如实运行,见内文)
cd(){
    
    
    cd "$@"    #真的改变目录了吗?
    x=$(pwd)    #取得当前目录的名称,并传给变量x
    PS1="${x##*/}\$"    #截断前面的组成部分后,指定给PS1
}

这里有一点美中不足:Shell函数如何访问真正的cd命令?这里所显示的cd "$@"只会再次调用此函数,导致无限递归。我们需要的转义策略,告诉Shell要避开函数的查找并访问真正的命令。这正是内建命令command的工作

#cd ---改变目录时更新PS1的私人版本
cd(){
    
    
    command cd "$@"  #实际改变目录
    x=$(pwd)         #取得当前目录的名称,传递给变量x
    PS1="${x##*/}\$" #截断前面的组成部分,指定给PS1
}

insert image description here

POSIX标准为特殊内建命令提供两个附加特性:

  • 特殊内建工具语法上的错误,会导致Shell执行该工具时退出,然而当语法错误出现在一般内建命令时,并不会导致Shell执行该工具时退出。如果特殊内建工具遇到语法错误时不退出Shell,则它的退出值应该非零。

  • 以特殊内建命令所标明的变量指定,在内建命令完成之后仍会有影响;这种情况不会发生在一般内建命令或其他工具的情况下。

第二项需要解释一下,你可以在命令前面指定一个变量复制,且变量值在被指定命令的环境中不影响当前Shell内的变量或是接下来的命令:

PATH=/bin:/usr/bin:/usr/ucb awk '...'

然而,当这样的指定用于特殊内建命令时,即使用在特殊内建命令完成之后,仍然会有影响。

下表列出了到目前为止尚未介绍的命令。这些命令中的大部分对于Shell脚本而言为特殊情况或不相关情况,不过我们在这里还是做一些简介,让你了解他们在做什么以及使用他们的时机:

命令 介绍
alias、unalias 分别用于别名的定义和删除。当命令被读取时,Shell会展开别名定义。别名主要用于交互式Shell;例如alias ‘rm=rm -i’ ,指的是强制rm在执行时要进行确认。Shell不会递归的别名展开,因此此定义是合法的。
bg、fg、jobs、kill 这些命令用于工作控制,他是一个操作系统工具,可将工作移到后台执行,或由后台执行中移出。
fc 是“fix command”的缩写,该命令设计用来交互模式下使用。它管理Shell之前已存储的执行过的命令历史,允许交互式用户再次调用先前用过的命令,编辑它以及再重新执行它
times 该命令会打印由Shell及所有子进程所累积执行迄今的CPU时间。它对于日常的脚本编写不是那么有用。
umask 用来设置文件建立时的权限掩码

剩下的两个命令在脚本中是用得到的。第一个是用来等待后台程序完成的wait命令。如未加任何参数,wait会等待所有的后台工作完成;否则,每个参数可以是后台工作的进程编号,或是工作控制的工作规格。

最后,.点号也是非常重要的命令。它是用来读取与执行包含在各别文件中的命令。例如,当你有很多个Shell函数想要在多个脚本中使用时,正确的方式是将它们放在各自的库文件里,再以点号命令来读取它们:

.my_funcs  #在函数中读取

如指定的文件未含斜杠,则Shell会查找$PATH下的目录,以找到该文件。该文件无须是可执行的,只要是可读取的即可。

set命令

set命令可以做的事相当广泛。就连使用的选项语法也与众不同,这是POSIX为保留历史的兼容性而存在的命令。也因为这样,这个命令有点难懂。

set命令最简单的工作就是以排序的方式显示所有Shell变量的名称与值。这是调用它时不加任何选项与参数时的行为。其输出采用Shell稍后可以重读的形式——包含适当的引号。这个想法出自Shell脚本有可能需要存储它的状态,在之后会通过.(点号)命令恢复它。

set的另一项任务是改变位置参数($1、$2等)。使用--的第一个参数来结束设置它自己的选项,则所有接下来的参数都会取代位置参数,即使它们是以正号或负号开头。

insert image description here

Finally, setshell options are used to turn on or off, which refer to internal settings that change the behavior of the shell. Here's where things get complicated. Historically, shell options have been described with a single letter, opened with a minus sign and closed with a plus sign. POSIX also added long options, which are turned on or off with -oor +o. Each single letter option has a corresponding long name option. The table below lists some of the options with a short description of their function.

short option oform illustrate
-a allexport output all subsequently defined variables
-b notify Immediately display job completion information instead of waiting for the next prompt number. for interactive use
-C noclobber Do not allow > to redirect to a file that already exists. The >| operator invalidates the setting of this option. For interactive use.
-e raised Exits the shell when the command exits with a non-zero status
-f noglob Disable wildcard expansion
-h When a function is defined (rather than when the function is executed), look up and remember where the command was called from within the function body.
-m monitor Open job control. for interactive use
-n noexec Read commands and check for syntax errors, but don't execute them. Interactive shells are allowed to ignore this option.
-u nounset Treat undefined variables as errors, not null
-v verbose print command before executing
-x xtrace Show commands before executing
ignoreeof Do not allow exiting the shell with Ctrl-D
nolog Turn off command history for function definitions
vi Use vi-style command line editing. for interactive use

What may surprise you is that setit is not used to set Shell variables. variable=valueThe work is achieved by simple specification.

The special variable $-is a string representing the currently opened shell options. The short option letter for each option will appear in the string (assuming the option is on). This can be used to test option settings like so:

case $- in
*C*)    ...  #启用noclobber选项
        ;;
esac

Guess you like

Origin blog.csdn.net/m0_56145255/article/details/128240210