#《快乐的 Linux 命令行》

#[[]]

[[string1 =~ regex]]可以匹配正则表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash

# test-integer2: evaluate the value of an integer.

INT=-5
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
if [ $INT -eq 0 ]; then
echo "INT is zero."
else
if [ $INT -lt 0 ]; then
echo "INT is negative."
else
echo "INT is positive."
fi
if [ $((INT % 2)) -eq 0 ]; then
echo "INT is even."
else
echo "INT is odd."
fi fi
else
echo "INT is not an integer." >&2
exit 1
fi
1
2
3
4
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ chmod +x test-integer2.sh
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./test-integer2.sh
INT is negative.
INT is odd.

[[ ]]添加的另一个功能是==操作符支持类型匹配。

1
2
3
4
5
6
7
#!/bin/bash

FILE=foo.bar

if [[ $FILE == foo.* ]]; then
echo "$FILE matches pattern 'foo.*'"
fi
1
2
3
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ chmod +x test-equal.sh
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./test-equal.sh
foo.bar matches pattern 'foo.*'

#(( ))

(( ))被用来执行算术真测试,如果算术计算的结果是非零值,则其测试值为真。

1
2
3
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ if ((1)); then echo "It is true."; fi
It is true.
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ if ((0)); then echo "It is true."; f

#read

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
$ read --help
read: read [-ers] [-a array] [-d delim] [-i text] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [-u fd] [name ...]
Read a line from the standard input and split it into fields.

Reads a single line from the standard input, or from file descriptor FD
if the -u option is supplied. The line is split into fields as with word
splitting, and the first word is assigned to the first NAME, the second
word to the second NAME, and so on, with any leftover words assigned to
the last NAME. Only the characters found in $IFS are recognized as word
delimiters.

If no NAMEs are supplied, the line read is stored in the REPLY variable.

Options:
-a array assign the words read to sequential indices of the array
variable ARRAY, starting at zero
-d delim continue until the first character of DELIM is read, rather
than newline
-e use Readline to obtain the line in an interactive shell
-i text use TEXT as the initial text for Readline
-n nchars return after reading NCHARS characters rather than waiting
for a newline, but honor a delimiter if fewer than
NCHARS characters are read before the delimiter
-N nchars return only after reading exactly NCHARS characters, unless
EOF is encountered or read times out, ignoring any
delimiter
-p prompt output the string PROMPT without a trailing newline before
attempting to read
-r do not allow backslashes to escape any characters
-s do not echo input coming from a terminal
-t timeout time out and return failure if a complete line of
input is not read within TIMEOUT seconds. The value of the
TMOUT variable is the default timeout. TIMEOUT may be a
fractional number. If TIMEOUT is 0, read returns
immediately, without trying to read any data, returning
success only if input is available on the specified
file descriptor. The exit status is greater than 128
if the timeout is exceeded
-u fd read from file descriptor FD instead of the standard input

Exit Status:
The return code is zero, unless end-of-file is encountered, read times out
(in which case it's greater than 128), a variable assignment error occurs,
or an invalid file descriptor is supplied as the argument to -u.

read命令可以用来读取键盘输入,当使用重定向的时候,读取文件中的一行数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/bash

# read-integer: evaluate the value of an integer.

echo -n "Please enter an integer -> "
read int
if [[ "$int" =~ ^-?[0-9]+$ ]]; then
if [ $int -eq 0 ]; then
echo "$int is zero."
else
if [ $int -lt 0 ]; then
echo "$int is negative."
else
echo "$int is positive."
fi
if [ $((int % 2)) -eq 0 ]; then
echo "$int is even."
else
echo "$int is odd."
fi
fi
else
echo "Input value is not an integer." >&2
exit 1
fi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ chmod +x read-integer.sh
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./read-integer.sh
Please enter an integer -> q
Input value is not an integer.
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./read-integer.sh
Please enter an integer -> 1
1 is positive.
1 is odd.
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./read-integer.sh
Please enter an integer -> 0
0 is zero.
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./read-integer.sh
Please enter an integer -> -5
-5 is negative.
-5 is odd.

如果read命令接受到变量值数目少于期望的数字,那么额外的变量值为空,而多余的输入数据则会被包含到最后一个变量中。

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

# read-multiple: read multiple values from keyboard

echo -n "Enter one or more values > "
read var1 var2 var3 var4 var5
echo "var1 = '$var1'"
echo "var2 = '$var2'"
echo "var3 = '$var3'"
echo "var4 = '$var4'"
echo "var5 = '$var5'"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ chmod +x read-multiple.sh
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./read-multiple.sh
Enter one or more values > a b c d e
var1 = 'a'
var2 = 'b'
var3 = 'c'
var4 = 'd'
var5 = 'e'
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./read-multiple.sh
Enter one or more values > a
var1 = 'a'
var2 = ''
var3 = ''
var4 = ''
var5 = ''
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./read-multiple.sh
Enter one or more values > a b c d e f g
var1 = 'a'
var2 = 'b'
var3 = 'c'
var4 = 'd'
var5 = 'e f g

如果read命令之后没有列出变量名,则一个shell变量REPLY将会包含所有的输入。

1
2
3
4
5
6
7
#!/bin/bash

# read-single: read multiple values into default variable

echo -n "Enter one or more values > "
read
echo "REPLY = '$REPLY'"
1
2
3
4
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ chmod +x read-single.sh
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./read-single.sh
Enter one or more values > a b c d
REPLY = 'a b c d'

-p选项,指定提示信息:

1
2
3
4
5
6
#!/bin/bash

# read-single: read multiple values into default variable

read -p "Enter one or more values > "
echo "REPLY = '$REPLY'"
1
2
3
4
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ chmod +x read-single2.sh
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./read-single2.sh
Enter one or more values > 127309 218741
REPLY = '127309 218741'

-t指定超时时间。
-s指定不在屏幕上显示输入的字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

# read-secret: input a secret pass phrase

echo "$(date +%Y%m%d-%H%M%S)"

if read -t 10 -sp "Enter secret pass phrase > " secret_pass; then
echo "\nSecret pass phrase = '$secret_pass'"
else
echo "$(date +%Y%m%d-%H%M%S)"
echo "\nInput timed out" >&2
exit 1
fi
1
2
3
4
5
6
7
8
9
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ chmod +x read-secret.sh
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./read-secret.sh
20210426-154024
Enter secret pass phrase > 20210426-154034
\nInput timed out
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./read-secret.sh
20210426-154040
Enter secret pass phrase > \nSecret pass phrase = 'qqqaqwerqwe'
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$

#IFS

内部字符分隔符,默认值包含一个空格、一个tab和一个换行符,每一个都会把字段分割开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash

# read-ifs: read fields from a file

FILE=/etc/passwd
read -p "Enter a user name > " user_name
file_info=$(grep "^$user_name:" $FILE)
if [ -n "$file_info" ]; then
IFS=":" read user pw uid gid name home shell <<<"$file_info"
echo "User = '$user'"
echo "UID = '$uid'"
echo "GID = '$gid'"
echo "Full Name = '$name'"
echo "Home Dir. = '$home'"
echo "Shell = '$shell'"
else
echo "No such user '$user_name'" >&2
exit 1
fi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ chmod +x read-ifs.sh
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./read-ifs.sh
Enter a user name > onns
No such user 'onns'
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./read-ifs.sh
Enter a user name > hs
User = 'hs'
UID = '1000'
GID = '1000'
Full Name = 'hs,,,'
Home Dir. = '/home/hs'
Shell = '/bin/bash'
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./read-ifs.sh
Enter a user name > git
User = 'git'
UID = '1001'
GID = '1001'
Full Name = ',,,'
Home Dir. = '/home/git'
Shell = '/usr/bin/git-shell'

shell允许在一个命令之前给一个或多个变量赋值。这些赋值会暂时改变之后的命令的环境变量。

1
2
3
4
5
6
7
8
IFS=":" read user pw uid gid name home shell <<<"$file_info"

# 等价于👇

OLD_IFS="$IFS"
IFS=":"
read user pw uid gid name home shell <<< "$file_info"
IFS="$OLD_IFS"

不能把管道用在read上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/bin/bash

# read-validate: validate input

invalid_input() {
echo "Invalid input '$REPLY'" >&2
exit 1
}

read -p "Enter a single item > "

# input is empty (invalid)
[[ -z $REPLY ]] && invalid_input

# input is multiple items (invalid)
(($(echo $REPLY | wc -w) > 1)) && invalid_input

# is input a valid filename?
if [[ $REPLY =~ ^[-[:alnum:]\._]+$ ]]; then
echo "'$REPLY' is a valid filename."
if [[ -e $REPLY ]]; then
echo "And file '$REPLY' exists."
else
echo "However, file '$REPLY' does not exist."
fi

# is input a floating point number?
if [[ $REPLY =~ ^-?[[:digit:]]*\.[[:digit:]]+$ ]]; then
echo "'$REPLY' is a floating point number."
else
echo "'$REPLY' is not a floating point number."
fi

# is input an integer?
if [[ $REPLY =~ ^-?[[:digit:]]+$ ]]; then
echo "'$REPLY' is an integer."
else
echo "'$REPLY' is not an integer."
fi
else
echo "The string '$REPLY' is not a valid filename."
fi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ chmod +x read-validate.sh
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./read-validate.sh
Enter a single item > read-validate.sh
'read-validate.sh' is a valid filename.
And file 'read-validate.sh' exists.
'read-validate.sh' is not a floating point number.
'read-validate.sh' is not an integer.
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./read-validate.sh
Enter a single item > -1.234
'-1.234' is a valid filename.
However, file '-1.234' does not exist.
'-1.234' is a floating point number.
'-1.234' is not an integer.
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./read-validate.sh
Enter a single item > -3
'-3' is a valid filename.
However, file '-3' does not exist.
'-3' is not a floating point number.
'-3' is an integer.
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./read-validate.sh
Enter a single item > ,,..eqwehu
The string ',,..eqwehu' is not a valid filename.

#追踪

set命令加上-x选项来启动追踪,+x选项关闭追踪。

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash

# trouble: script to demonstrate common errors

number=1
set -x # Turn on tracing
if [ $number = 1 ]; then
echo "Number is equal to 1."
else
echo "Number is not equal to 1."
fi
set +x # Turn off tracing
1
2
3
4
5
6
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ chmod +x trouble.sh
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./trouble.sh
+ '[' 1 = 1 ']'
+ echo 'Number is equal to 1.'
Number is equal to 1.
+ set +x

#case

1
2
3
case word in
[pattern [| pattern]...) commands ;;]...
esac
模式 描述
a) 若单词为a,则匹配
[[:alpha:]]) 若单词是一个字母字符,则匹配
???) 若单词只有3个字符,则匹配
*.txt) 若单词以.txt字符结尾,则匹配
*) 匹配任意单词

添加;;&的语法允许case语句继续执行下一条测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash

# case4-2: test a character

read -n 1 -p "Type a character > "
echo
case $REPLY in
[[:upper:]]) echo "'$REPLY' is upper case." ;;&
[[:lower:]]) echo "'$REPLY' is lower case." ;;&
[[:alpha:]]) echo "'$REPLY' is alphabetic." ;;&
[[:digit:]]) echo "'$REPLY' is a digit." ;;&
[[:graph:]]) echo "'$REPLY' is a visible character." ;;&
[[:punct:]]) echo "'$REPLY' is a punctuation symbol." ;;&
[[:space:]]) echo "'$REPLY' is a whitespace character." ;;&
[[:xdigit:]]) echo "'$REPLY' is a hexadecimal digit." ;;&
esac
1
2
3
4
5
6
7
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ chmod +x case4-2.sh
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./case4-2.sh
Type a character > a
'a' is lower case.
'a' is alphabetic.
'a' is a visible character.
'a' is a hexadecimal digit.

#访问命令行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash

# posit-param: script to view command line parameters

echo "\$0 = $0
\$1 = $1
\$2 = $2
\$3 = $3
\$4 = $4
\$5 = $5
\$6 = $6
\$7 = $7
\$8 = $8
\$9 = $9"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ chmod +x posit-param.sh
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./posit-param.sh
$0 = ./posit-param.sh
$1 =
$2 =
$3 =
$4 =
$5 =
$6 =
$7 =
$8 =
$9 =
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./posit-param.sh a b c d
$0 = ./posit-param.sh
$1 = a
$2 = b
$3 = c
$4 = d
$5 =
$6 =
$7 =
$8 =
$9 =

超过9的变量用花括号包裹${10}

$#可以确定参数个数。

执行一次shift命令可以让所有的位置参数向下移动一个位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash

# posit-param: script to view command line parameters

echo "\$0 = $0
\$1 = $1
\$2 = $2
\$3 = $3
\$4 = $4
\$5 = $5
\$6 = $6
\$7 = $7
\$8 = $8
\$9 = $9"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ chmod +x posit-param2.sh
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./posit-param2.sh
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./posit-param2.sh a b c d
Argument 1 = a
Argument 2 = b
Argument 3 = c
Argument 4 = d
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./posit-param2.sh *
Argument 1 = case4-2.sh
Argument 2 = dirlist-bin.txt
Argument 3 = dirlist-sbin.txt
Argument 4 = dirlist-usr-bin.txt
Argument 5 = dirlist-usr-sbin.txt
Argument 6 = distros-by-date.txt
Argument 7 = distros-dates.txt
Argument 8 = distros-key-names.txt
Argument 9 = distros-key-vernums.txt
Argument 10 = distros-names.txt
Argument 11 = distros-vernums.txt
Argument 12 = distros-versions.txt
Argument 13 = distros.txt
Argument 14 = file1.txt
Argument 15 = file2.txt
Argument 16 = fmt-code.txt
Argument 17 = ngrok
Argument 18 = onns.txt
Argument 19 = p.tar
Argument 20 = p2.tar
Argument 21 = patchfile.txt
Argument 22 = phonelist.txt
Argument 23 = playground
Argument 24 = posit-param.sh
Argument 25 = posit-param2.sh
Argument 26 = read-ifs.sh
Argument 27 = read-integer.sh
Argument 28 = read-multiple.sh
Argument 29 = read-secret.sh
Argument 30 = read-single.sh
Argument 31 = read-single2.sh
Argument 32 = read-validate.sh
Argument 33 = t.sh
Argument 34 = test-equal.sh
Argument 35 = test-file.sh
Argument 36 = test-integer.sh
Argument 37 = test-integer2.sh
Argument 38 = test-string.sh
Argument 39 = test_local.sh
Argument 40 = trouble.sh
Argument 41 = vfos

$*展开成一个从1开始的位置参数列表。当它被用双引号引起来的时候,展开成一个由双引号引起来的字符串,包含了所有的位置参数,每个位置参数由shell变量IFS的第一个字符(默认为一个空格)分隔开。

%@展开成一个从1开始的位置参数列表。当它被用双引号引起来的时候,它把每一个位置参数展开成一个由双引号引起来的分开的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash

# posit-params3 : script to demonstrate $* and $@

print_params() {
echo "\$1 = $1"
echo "\$2 = $2"
echo "\$3 = $3"
echo "\$4 = $4"
}
pass_params() {
echo -e "\n" '$* :'; print_params $*
echo -e "\n" '"$*" :'; print_params "$*"
echo -e "\n" '$@ :'; print_params $@
echo -e "\n" '"$@" :'; print_params "$@"
}

pass_params "word" "words with spaces"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ chmod +x posit-params3.sh
(base) hs@hs-Z390-AORUS-PRO:/mnt/data/onns/Desktop$ ./posit-params3.sh

$* :
$1 = word
$2 = words
$3 = with
$4 = spaces

"$*" :
$1 = word words with spaces
$2 =
$3 =
$4 =

$@ :
$1 = word
$2 = words
$3 = with
$4 = spaces

"$@" :
$1 = word
$2 = words with spaces
$3 =
$4 =

$@在大多数情况下是最有用的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#!/bin/bash

# sys_info_page: program to output a system information page

PROGNAME=$(basename $0)
TITLE="System Information Report For $HOSTNAME"
CURRENT_TIME=$(date +"%x %r %Z")
TIMESTAMP="Generated $CURRENT_TIME, by $USER"
report_uptime() {
cat <<-_EOF_
<H2>System Uptime</H2>
<PRE>$(uptime)</PRE>
_EOF_
return
}
report_disk_space() {
cat <<-_EOF_
<H2>Disk Space Utilization</H2>
<PRE>$(df -h)</PRE>
_EOF_
return
}
report_home_space() {
if [[ $(id -u) -eq 0 ]]; then
cat <<-_EOF_
<H2>Home Space Utilization (All Users)</H2>
<PRE>$(du -sh /home/*)</PRE>
_EOF_
else
cat <<-_EOF_
<H2>Home Space Utilization ($USER)</H2>
<PRE>$(du -sh $HOME)</PRE>
_EOF_
fi
return
}
usage() {
echo "$PROGNAME: usage: $PROGNAME [-f file | -i]"
return
}
write_html_page() {
cat <<-_EOF_
<HTML>
<HEAD>
<TITLE>$TITLE</TITLE>
</HEAD>
<BODY>
<H1>$TITLE</H1>
<P>$TIMESTAMP</P>
$(report_uptime)
$(report_disk_space)
$(report_home_space)
</BODY>
</HTML>
_EOF_
return
}
# process command line options
interactive=
filename=
while [[ -n $1 ]]; do
case $1 in
-f | --file)
shift
filename=$1
;;
-i | --interactive)
interactive=1
;;
-h | --help)
usage
exit
;;
*)
usage >&2
exit 1
;;
esac
shift
done
# interactive mode
if [[ -n $interactive ]]; then
while true; do
read -p "Enter name of output file: " filename
if [[ -e $filename ]]; then
read -p "'$filename' exists. Overwrite? [y/n/q] > "
case $REPLY in
Y | y)
break
;;
Q | q)
echo "Program terminated."
exit
;;
*)
continue
;;
esac
fi
done
fi
# output html page
if [[ -n $filename ]]; then
if touch $filename && [[ -f $filename ]]; then
write_html_page >$filename
else
echo "$PROGNAME: Cannot write file '$filename'" >&2
exit 1
fi
else
write_html_page
fi

17:02:15

454/505