高階shell程式設計筆記(第三十三章 雜項)

小吳同學buhei發表於2020-11-06

第三十三章 雜項

33.1 互動式和非互動式的shell和指令碼

互動式的shell在tty終端從使用者的輸入中讀取命令。另一方面,shell能在啟動時讀取啟動檔案,顯示一個提示符並預設啟用作業控制。使用者能互動地使用shell。

執行指令碼的shell一般都是非互動的shell。但指令碼仍然可以存取他擁有的終端。指令碼里甚至可以仿效成可互動的shell。

#!/bin/bash
#
MY_PROMPT='$ '

while :
do
  echo -n "$MY_PROMPT"
  read line
  eval "$line"
done
exit 0  

初始化和啟動指令碼是非互動式的,因為它們必須不需要人為地干預地執行。許多管理和系統維護指令碼也同樣是非互動式的。不多變的重複性的任務可以自動地由非互動式指令碼完成。

非互動式的指令碼可以在後臺執行,但互動指令碼在後臺執行則會被掛起,等待永遠不會到達的輸入。解決這個難點的辦法可以寫預料這種情況的指令碼或是內嵌 here document 的指令碼來獲取指令碼期望的輸入,這樣就可作為後臺任務執行了。在最簡單的情況,重定向一個檔案給一個 read 語句提供輸入(read variable < file)。這就可能適應互動和非互動的工作環境下都能達成指令碼執行的目的。

如果指令碼需要測試當前是否執行在互動 shell 中,一個簡單的辦法是找一下是否有提示符變數,即$PS1 是否設定了。(如果指令碼需要使用者輸入資料,則指令碼會顯示一個提示符.)

if [ -z $PS1 ];then   #沒有提示符?
#非互動式
  ...
else
#互動式
  ...
fi    

另一個辦法是指令碼可以測試是否在變數$-中出現了選項"i"。

case $- in
*i*) ...;;   #互動式
*) ...;;   #非互動式

注意:指令碼可以使用-i 選項強制在互動模式下執行或指令碼頭用#!/bin/bash -i。注意這樣可能會引起指令碼古怪的行為或當沒有錯誤出現時也會顯示錯誤資訊。

33.2 Shell包裝

包裝指令碼是指嵌有一個系統命令和程式的指令碼,也儲存了一組傳給該命令的引數。包裝指令碼是原來很複雜的命令列簡單化。這對sed和awk特別有用。

sed和awk命令一般從命令列上以sed -e 'commands’和awk 'commands’來呼叫。把sed和awk的命令嵌入到Bash指令碼里使呼叫變得更簡單,並且也可以多次使用。也可以綜合的利用sed和awk的功能。例如管道(piping)連線 sed 命令的輸出到 awk 命令中。儲存為可執行的檔案,你可以用指令碼編寫的或修改的呼叫格式多次的呼叫它,而不必在命令列上重複鍵入複雜的命令列。

Example 33-1 shell包裝

#!/bin/bash
#
#這個是一個把檔案中的空行刪除的簡單指令碼
#沒有引數

#
E_NOARGS=65
if [ -z "$1" ];then
  echo "Usage: `basename $0` target-file"
  exit $E_NOARGS
fi

sed -e /^$/d "$1"
exit 0  

Example 33-2 稍微複雜一些的shell包裝

#!/bin/bash
#
ARGS=3   #指令碼要求三個引數.
E_BADARGS=65   #傳遞了錯誤的引數個數給指令碼.

if [ $# -ne "$ARGS" ];then
  echo "Usage: `basename $0` old-pattern new-pattern filename"
  exit $E_BADARGS
fi

old_pattern=$1
new_pattern=$2

if [ -f "$3" ];then
  file_name=$3
else
  echo "File \"$3\" does not exist."
  exit $E_BADARGS
fi

#這裡是實現功能的程式碼
sed -e "s/$old_pattern/$new_pattern/g" $file_name
exit 0

Example 33-3 寫到日誌檔案的shell包裝

#!/bin/bash
#
#普通的 shell 包裝,執行一個操作並記錄在日誌裡

#需要設定下面的兩個變數.
OPERATION=
#可以是一個複雜的命令鏈,例如 awk 指令碼或是管道 . . .
LOGFILE=
#不管怎麼樣,命令列引數還是要提供給操作的.
OPTIONS="$@"

#記錄操作
echo "`date` + `whoami` + $OPERATION "$@"" >> $LOGFILE
#現在執行操作
exec $OPERATION "$@""


Example 33-4 包裝awk的指令碼
#!/bin/bash
#
#列印 ASCII 碼的字元表.
START=33
END=125

echo "Decimal   Hex     Character"   #表頭
echo "-----     ---     ----------"

for ((i=START;i<=END;i++))
do
  echo $i | awk '{printf(" %3d      %2x           %c\n",$1,$1,$1)}'
done
exit 0  

Example 33-5 另一個包裝awk的指令碼

#!/bin/bash
#
#給目標檔案增加一列由數字指定的列.

ARGS=2
E_WRONGARGS=65

if [ $# -ne "$ARGS" ];then
  echo "Usage: `basename $0` filename column-number"
  exit $E_WRONGARGS
fi

filename=$1
column_number=$2
#
#傳遞 shell 變數給指令碼的 awk 部分需要一點技巧.
#  方法之一是在 awk 指令碼中使用強引用來引起 bash 指令碼的變數
#  $'$BASH_SCRIPT_VAR'

awk '
{total += $'"$column_number"'
}
END{
    print total
}' "$filename"

#把 shell 變數傳遞給 awk 變數可能是不安全的,
#因此 Stephane Chazelas 提出了下面另外一種方法:
# awk -v column_number="$column_number" '
# {total += $column_number
# }
# END{
#    print total
#}' "$filename"
exit 0

Perl 兼有 sed 和 awk 的能力,並且具有 C 的一個很大的子集。它是標準的並支援物件導向程式設計的方方面面,甚至是很瑣碎的東西。 短的 Perl 指令碼也可以嵌入到 shell 指令碼中去,以至於有些人宣稱 Perl 能夠完全地代替 shell 程式設計。

Example 33-6 把Perl嵌入Bash指令碼

#!/bin/bash
#
echo "This precedes the embedded Perl script within \"$0\"."
echo "======================================================"
perl -e 'print "This is an embedded Perl script.\n";'
#sed 指令碼, Perl 也使用"-e"選項.
echo "======================================================"
1 echo "However, the script may also contain shell and system commands."
exit 0

把 Bash 指令碼和 Perl 指令碼放在同一個檔案是可能的。依賴於指令碼如何被呼叫,要麼是 Bash 部分被執行,要麼是 Perl 部分被執行。

Example 33-7 Bash和Perl指令碼聯合使用

#!/bin/bash
#
echo "Greetings from the Bash part of the script."
#...
exit 0
#指令碼的 Bash 部分結束.
#==================================================

#!/usr/bin/perl
# 指令碼的這個部分必須用-x 選項來呼叫.
print "Greetings from the Perl part of the script.\n"
#下面可以有更多的 Perl 命令.
#...
#指令碼的 Perl 部分結束.

下面是執行結果
(base) [root@zhhs-mail shell]# sh test.sh 
Greetings from the Bash part of the script.
(base) [root@zhhs-mail shell]# perl -x test.sh 
Greetings from the Perl part of the script.

33.3 測試和比較:另一種方法

對於測試,[[ ]]結構可能比[ ]更合適。同樣地,算術比較可能用(( ))結構更有用。

a=8
#下面所有的比較是等價的
test "$a" -lt 16 && echo "yes,$a < 16"
/bin/test "$a" -lt 16 && echo "yes,$a < 16"
[ "$a" -lt 16 ] && echo "yes,$a < 16"
[[ $a -lt 16 ]] && echo "yes,$a < 16"   ## 在[[ ]]和(( ))中不必用引號引起變數
((a < 16)) && echo "yes,$a < 16"

city="New York"
#同樣,下面的所有比較都是等價的.
test "$city" \< Paris && echo "Yes, Paris is greater than $city"
/bin/test "$city" \< Paris && echo "Yes, Paris is greater than $city"
[ "$city" \< Paris ] && echo "Yes, Paris is greater than $city"
[[ $city < Paris ]] && echo "Yes, Paris is greater than $city"   # 不需要用引號引起$city.

33.4 遞迴

指令碼能遞迴地呼叫自己本身。

Example 33-8 遞迴呼叫自己本身的(沒用)指令碼

#!/bin/bash
#
RANGE=10
MAXVAL=9

i=$RANDOM
let "i %= $RANGE"   #產生一個從 0 到 $RANGE - 1 之間的隨機數.

if [ "$i" -lt "$MAXVAL" ];then
  echo "i = $i"
  ./$0   #指令碼遞迴地呼叫再生成一個和自己一樣的例項.
fi
#每個子指令碼做的事都一樣,直到產生的變數 $i 和變數 $MAXVAL 相等.
exit 0

Example 33-9 遞迴呼叫自己本身的(有用)指令碼

#!/bin/bash
#
MINARGS=1   #指令碼需要至少一個引數
DATAFILE=./phonebook   #在當前目錄下名為"phonebook"的資料檔案必須存在

PROGNAME=$0
E_NOARGS=70   #沒有引數的錯誤值

if [ $# -lt $MINARGS ];then
  echo "Usage: "$PROGNAME" data"
  exit $E_NOARGS
fi

if [ $# -eq $MINARGS ];then
  grep $1 "$DATAFILE"
else
  ( shift; "$PROGNAME" $* ) | grep $1
fi
exit 0

#-------------------------
"phonebook"檔案的例子:
John Doe 1555 Main St., Baltimore, MD 21228 (410) 222-3333
Mary Moe 9899 Jones Blvd., Warren, NH 03787 (603) 898-3232
Richard Roe 856 E. 7th St., New York, NY 10009 (212) 333-4567
Sam Roe 956 E. 8th St., New York, NY 10009 (212) 444-5678
Zoe Zenobia 4481 N. Baker St., San Francisco, SF 94338 (415) 501-1631
#--------------------------

33.5 彩色指令碼

ANSI定義了螢幕屬性的轉義序列集合,例如:粗體文字,背景和前景顏色。DOS 批處理檔案(batch files) 一般使用 ANSI 的轉義程式碼來控制色彩輸出,Bash 指令碼也是這麼做的。

Example 33-11 一個 “彩色的” 地址資料庫

#!/bin/bash
#
clear
echo -n "                "
echo -e '\E[37;44m'"\033[1mContact List\033[0m"   #白色為前景色,藍色為背景色
echo
echo
echo -e "\033[1mChoose one of the following persons:\033[0m"   #粗體

tput sgr0
echo "(只輸入姓名的第一個字母.)"
echo
echo -en '\E[47;34m'"\033[1mE\033[0m"   #藍色
tput sgr0   #把色彩設定為"常規"
echo "vans, Roland"
echo -en '\E[47;35m'"\033[1mJ\033[0m"   #紅紫色
tput sgr0
echo "ones, Mildred"
echo -en '\E[47;32m'"\033[1mS\033[0m"   #綠色
tput sgr0
echo "mith, Julie"
echo -en '\E[47;31m'"\033[1mZ\033[0m"   #紅色
tput sgr0
echo "ane, Morris"
echo

read person

case "$person" in
"E" | "e" )
# 接受大小寫的輸入.
echo
echo "Roland Evans"
echo "4321 Floppy Dr."
echo "Hardscrabble, CO 80753"
echo "(303) 734-9874"
echo "(303) 734-9892 fax"
echo "revans@zzy.net"
echo "Business partner & old friend"
;; 
"J" | "j" )
echo
echo "Mildred Jones"
echo "249 E. 7th St., Apt. 19"
echo "New York, NY 10009"
echo "(212) 533-2814"
echo "(212) 533-9972 fax"
echo "milliej@loisaida.com"
echo "Girlfriend"
echo "Birthday: Feb. 11"
;; 
*)
echo
echo "Not yet in database."
;;
esac
tput sgr0
echo
exit 0

Example 33-12 畫盒子(複製貼上的)

#!/bin/bash
# 用 ASCII 字元畫一個盒子.

### draw_box 函式的註釋 ###
# "draw_box" 函式使使用者可以在終端上畫一個盒子.
#
# 用法: draw_box ROW COLUMN HEIGHT WIDTH [COLOR]
# ROW 和 COLUMN 定位要畫的盒子的左上角.
#
# ROW 和 COLUMN 必須要大於 0 且小於目前終端的尺寸.
#
# HEIGHT 是盒子的行數,必須 > 0.
# HEIGHT + ROW 必須 <= 終端的高度.
# WIDTH 是盒子的列數,必須 > 0.
# WIDTH + COLUMN 必須 <= 終端的寬度.
#
# 例如: 如果你當前終端的尺寸是 20x80,
# draw_box 2 3 10 45 是合法的
# draw_box 2 3 19 45 的 HEIGHT 值是錯的 (19+2 > 20)
# draw_box 2 3 18 78 的 WIDTH 值是錯的 (78+3 > 80)
#
# COLOR 是盒子邊框的顏色.
# 它是第 5 個引數,並且它是可選的.
# 0=黑色 1=紅色 2=綠色 3=棕褐色 4=藍色 5=紫色 6=青色 7=白色.
# 如果你傳給這個函式錯的引數,
#+ 它就會以程式碼 65 退出,
#+ 沒有其他的資訊列印到標準出錯上.
#
# 在畫盒子之前要清屏.
# 函式內不包含有清屏命令.
# 這使使用者可以畫多個盒子,甚至疊接多個盒子.

### draw_box 函式註釋結束 ###

########################################################
draw_box(){
#=============#
HORZ="-"
VERT="|"
CORNER_CHAR="+"

MINARGS=4
E_BADARGS=65
#=============#
if [ $# -lt "$MINARGS" ]; then # 如果引數小於 4,退出.
  exit $E_BADARGS
fi

# 搜尋引數中的非數字的字元.
if echo $@ | tr -d [:blank:] | tr -d [:digit:] | grep . &> /dev/null; then
  exit $E_BADARGS
fi

BOX_HEIGHT=`expr $3 - 1` # -1 是需要的,因為因為邊角的"+"是高和寬共有的部分.
BOX_WIDTH=`expr $4 - 1` #
T_ROWS=`tput lines` # 定義當前終端長和寬的尺寸,
T_COLS=`tput cols` #

if [ $1 -lt 1 ] || [ $1 -gt $T_ROWS ]; then # 如果引數是數字就開始檢查有效性.
  exit $E_BADARGS #
fi
if [ $2 -lt 1 ] || [ $2 -gt $T_COLS ]; then
  exit $E_BADARGS
fi
if [ `expr $1 + $BOX_HEIGHT + 1` -gt $T_ROWS ]; then
  exit $E_BADARGS
fi
if [ `expr $2 + $BOX_WIDTH + 1` -gt $T_COLS ]; then
  exit $E_BADARGS
fi 
if [ $3 -lt 1 ] || [ $4 -lt 1 ]; then
  exit $E_BADARGS
fi # 引數檢查完畢.

plot_char(){ # 函式內的函式.
echo -e "\E[${1};${2}H"$3
}

echo -ne "\E[3${5}m" # 如果傳遞了盒子邊框顏色引數,則設定它.

# start drawing the box

count=1 # 用 plot_char 函式畫垂直線
for (( r=$1; count<=$BOX_HEIGHT; r++)); do
  plot_char $r $2 $VERT
  let count=count+1
done

count=1
c=`expr $2 + $BOX_WIDTH`
for (( r=$1; count<=$BOX_HEIGHT; r++)); do
  plot_char $r $c $VERT
  let count=count+1
done

count=1 # 用 plot_char 函式畫水平線
for (( c=$2; count<=$BOX_WIDTH; c++)); do #
  plot_char $1 $c $HORZ
  let count=count+1
done

count=1
r=`expr $1 + $BOX_HEIGHT`
for (( c=$2; count<=$BOX_WIDTH; c++)); do
  plot_char $r $c $HORZ
  let count=count+1
done

plot_char $1 $2 $CORNER_CHAR # 畫盒子的角.
plot_char $1 `expr $2 + $BOX_WIDTH` +
plot_char `expr $1 + $BOX_HEIGHT` $2 +
plot_char `expr $1 + $BOX_HEIGHT` `expr $2 + $BOX_WIDTH` +

echo -ne "\E[0m" # 恢復最初的顏色. 

P_ROWS=`expr $T_ROWS - 1` # 在終端的底部列印提示符.

echo -e "\E[${P_ROWS};1H"
}


# 現在, 讓我們來畫一個盒子.
clear # 清屏.
R=2 # 行
C=3 # 列
H=10 # 高
W=45 # 寬
col=1 # 顏色(紅)
draw_box $R $C $H $W $col # 畫盒子.

exit 0

最簡單也可能是最有用的 ANSI 轉義序列是加粗文字,\033[1m … \033[0m。\033 觸發轉義序列,而 “[1” 啟用加粗屬性, 而"[0" 表示切換回禁用加粗狀態。"m"則表示終止一個轉義序列。

echo -e "\033[1mThis is bold text.\033[0m"

一種相似的轉義序列可切換下劃線效果 (在 rxvt 和 aterm 上)。

echo -e "\033[4mThis is underlined text.\033[0m"

注意: echo 使用-e 選項可以啟用轉義序列。

其他的轉義序列可用於更改文字或/和背景色彩。

echo -e '\E[34;47mThis prints in blue.'; tput sgr0 
echo -e '\E[33;44m'"yellow text on blue background"; tput sgr0
echo -e '\E[1;33;44m'"BOLD yellow text on blue background"; tput sgr0

注意:通常為淡色的前景色文字設定粗體效果是較好的。

tput sgr0把終端設定恢復為原樣。如果省略這一句會使後續在該終端的輸出仍為藍色。

可以在有色的背景上用下面的模板寫有色彩的文字

echo -e '\E[COLOR1;COLOR2mSome text goes here.'

“\E[” 開始轉義序列。分號分隔的數值"COLOR1" 和 “COLOR2” 指定前景色和背景色,數值和色彩的對應參見下面的表格。(數值的順序不是有關係的,因為前景色和背景色數值都落在不重疊的範圍裡。) "m"終止該轉義序列,然後文字以結束的轉義指定的屬性顯示。

也要注意到用單引號引用了 echo -e 後面的餘下命令序列。

下表的數值是在 rxvt 終端執行的結果。具體效果可能在其他的各種終端上不一樣。

色彩前景色背景色
3040
3141
3242
3343
3444
洋紅3545
3646
3747

Example 33-13 顯示彩色文字

#!/bin/bash
#
#用彩色來顯示文字
black='\E[30;47m'
red='\E[31;47m'
green='\E[32;47m'
yellow='\E[33;47m'
blue='\E[34;47m' 
magenta='\E[35;47m'
cyan='\E[36;47m'
white='\E[37;47m' 

alias Reset="tput sgr0"   # 把文字屬性重設回原來沒有清屏前的

cecho()   #引數 $1 = 要顯示的資訊,引數 $2 = 顏色
{
    local default_msg="No message passed."
    message=${1:-$default_msg} # 預設的資訊.
    color=${2:-$black} # 如果沒有指定,預設使用黑色.
    
    echo -e "$color"
    echo "$message"
    Reset
    return
}
cecho "Feeling blue..." $blue
cecho "Magenta looks more like purple." $magenta
cecho "Green with envy." $green
cecho "Seeing red?" $red
cecho "Cyan, more familiarly known as aqua." $cyan
cecho "No color passed (defaults to black)."
# 缺失 $color (色彩)引數.
cecho "\"Empty\" color passed (defaults to black)." ""
# 空的 $color (色彩)引數.
cecho
# $message(資訊) 和 $color (色彩)引數都缺失.
cecho "" ""
# 空的 $message (資訊)和 $color (色彩)引數. 
exit 0

Example 33-14 "賽馬"遊戲(複製貼上的)

#!/bin/bash
# horserace.sh: 非常簡單的賽馬模擬.
# 作者: Stefano Palmeri
# 已取得使用許可.


#########################################################
#
# 指令碼目的:
# 使用轉義字元和終端顏色.
#
# 練習:
# 編輯指令碼使其更具有隨機性,
#+ 設定一個假的賭場 . . .
# 嗯 . . . 嗯 . . . 這個開始使我想起了一部電影 . . .
#
# 指令碼給每匹馬一個隨機的障礙.
# 不均等會以障礙來計算
#+ 並且用一種歐洲風格表達出來.
# 例如: 機率(odds)=3.75 意味著如果你押 1 美元贏,
#+ 你可以贏得 3.75 美元.
#
# 指令碼已經在 GNU/Linux 作業系統上測試過 OS,
#+ 測試終端有 xterm 和 rxvt, 及 konsole.
# 測試機器有 AMD 900 MHz 的處理器,
#+ 平均比賽時間是 75 秒.
# 在更快的計算機上比賽時間應該會更低.
# 所以, 如果你想有更多的懸念,重設 USLEEP_ARG 變數的值.
#
# 由 Stefano Palmeri 編寫. 

############################################################
####

E_RUNERR=65

# 檢查 md5sum 和 bc 是不是安裝了.
if ! which bc &> /dev/null; then
echo bc is not installed.
echo "Can\'t run . . . "
exit $E_RUNERR
fi
if ! which md5sum &> /dev/null; then
echo md5sum is not installed.
echo "Can\'t run . . . "
exit $E_RUNERR
fi

# 更改下面的變數值可以使指令碼執行的更慢.
# 它會作為 usleep 的引數 (man usleep)
#+ 並且它的單位是微秒 (500000 微秒 = 半秒).
USLEEP_ARG=0

# 如果指令碼接收到 ctrl-c 中斷,清除臨時目錄, 恢復終端游標和顏色
#
trap 'echo -en "\E[?25h"; echo -en "\E[0m"; stty echo;\
tput cup 20 0; rm -fr $HORSE_RACE_TMP_DIR' TERM EXIT
# 參考除錯的章節瞭解'trap'的更多解釋

# 給指令碼設定一個唯一(實際不是絕對唯一的)的臨時目錄名.
HORSE_RACE_TMP_DIR=$HOME/.horserace-`date +%s`-`head -c10 /dev/urandom |
md5sum | head -c30`

# 建立臨時目錄,並切換到該目錄下.
mkdir $HORSE_RACE_TMP_DIR
cd $HORSE_RACE_TMP_DIR


# 這個函式把游標移動到行為 $1 列為 $2 然後列印 $3.
# 例如: "move_and_echo 5 10 linux" 等同於
#+ "tput cup 4 9; echo linux", 但是用一個命令代替了兩個.
# 注: "tput cup" 表示在終端左上角的 0 0 位置,
#+ echo 是在終端的左上角的 1 1 位置.
move_and_echo() { 
echo -ne "\E[${1};${2}H""$3"
}

# 產生 1-9 之間偽隨機數的函式.
random_1_9 () {
head -c10 /dev/urandom | md5sum | tr -d [a-z] | tr -d 0 | cut -c1
}

# 畫馬時模擬運動的兩個函式.
draw_horse_one() {
echo -n " "//$MOVE_HORSE//
}
draw_horse_two(){
echo -n " "\\\\$MOVE_HORSE\\\\
}


# 取得當前的終端尺寸.
N_COLS=`tput cols`
N_LINES=`tput lines`

# 至少需要 20-行 X 80-列 的終端尺寸. 檢查一下.
if [ $N_COLS -lt 80 ] || [ $N_LINES -lt 20 ]; then
echo "`basename $0` needs a 80-cols X 20-lines terminal."
echo "Your terminal is ${N_COLS}-cols X ${N_LINES}-lines."
exit $E_RUNERR
fi


# 開始畫賽場.

# 需要一個 80 個字元的字串,看下面的.
BLANK80=`seq -s "" 100 | head -c80`

clear

# 把前景和背景顏色設定成白色的.
echo -ne '\E[37;47m'

# 把游標移到終端的左上角.
tput cup 0 0

# 畫六條白線.
for n in `seq 5`; do 
echo $BLANK80 # 線是用 80 個字元組成的字串.
done

# 把前景色設定成黑色.
echo -ne '\E[30m'

move_and_echo 3 1 "START 1"
move_and_echo 3 75 FINISH
move_and_echo 1 5 "|"
move_and_echo 1 80 "|"
move_and_echo 2 5 "|"
move_and_echo 2 80 "|"
move_and_echo 4 5 "| 2"
move_and_echo 4 80 "|"
move_and_echo 5 5 "V 3"
move_and_echo 5 80 "V"

# 把前景色設定成紅色.
echo -ne '\E[31m'

# 一些 ASCII 藝術.
move_and_echo 1 8 "..@@@..@@@@@...@@@@@.@...@..@@@@..."
move_and_echo 2 8 ".@...@...@.......@...@...@.@......."
move_and_echo 3 8 ".@@@@@...@.......@...@@@@@.@@@@...."
move_and_echo 4 8 ".@...@...@.......@...@...@.@......."
move_and_echo 5 8 ".@...@...@.......@...@...@..@@@@..."
move_and_echo 1 43 "@@@@...@@@...@@@@..@@@@..@@@@."
move_and_echo 2 43 "@...@.@...@.@.....@.....@....."
move_and_echo 3 43 "@@@@..@@@@@.@.....@@@@...@@@.."
move_and_echo 4 43 "@..@..@...@.@.....@.........@."
move_and_echo 5 43 "@...@.@...@..@@@@..@@@@.@@@@.."


# 把前景和背景顏色設為綠色.
echo -ne '\E[32;42m'

# 畫 11 行綠線.
tput cup 5 0
for n in `seq 11`; do
echo $BLANK80
done

# 把前景色設為黑色.
echo -ne '\E[30m' 
tput cup 5 0

# 畫柵欄.
echo "++++++++++++++++++++++++++++++++++++++\
++++++++++++++++++++++++++++++++++++++++++"

tput cup 15 0
echo "++++++++++++++++++++++++++++++++++++++\
++++++++++++++++++++++++++++++++++++++++++"

# 把前景和背景色設回白色.
echo -ne '\E[37;47m'

# 畫 3 條白線.
for n in `seq 3`; do
echo $BLANK80
done

# 把前景色設為黑色.
echo -ne '\E[30m'

# 建立 9 個檔案來儲存障礙物.
for n in `seq 10 7 68`; do
touch $n
done

# 設定指令碼要畫的馬的型別為第一種型別.
HORSE_TYPE=2

# 為每匹馬建立位置檔案和機率檔案.
#+ 在這些檔案裡儲存了該匹馬當前的位置,
#+ 型別和機率.
for HN in `seq 9`; do
touch horse_${HN}_position
touch odds_${HN}
echo \-1 > horse_${HN}_position
echo $HORSE_TYPE >> horse_${HN}_position
# 給馬定義隨機的障礙物.
HANDICAP=`random_1_9`
# 檢查 random_1_9 函式是否返回了有效值.
while ! echo $HANDICAP | grep [1-9] &> /dev/null; do
HANDICAP=`random_1_9`
done
# 給馬定義最後的障礙的位置. 
LHP=`expr $HANDICAP \* 7 + 3`
for FILE in `seq 10 7 $LHP`; do
echo $HN >> $FILE
done

# 計算機率.
case $HANDICAP in
1) ODDS=`echo $HANDICAP \* 0.25 + 1.25 | bc`
echo $ODDS > odds_${HN}
;;
2 | 3) ODDS=`echo $HANDICAP \* 0.40 + 1.25 | bc`
echo $ODDS > odds_${HN}
;;
4 | 5 | 6) ODDS=`echo $HANDICAP \* 0.55 + 1.25 | bc`
echo $ODDS > odds_${HN}
;;
7 | 8) ODDS=`echo $HANDICAP \* 0.75 + 1.25 | bc`
echo $ODDS > odds_${HN}
;;
9) ODDS=`echo $HANDICAP \* 0.90 + 1.25 | bc`
echo $ODDS > odds_${HN}
esac


done


# 印表機率.
print_odds() {
tput cup 6 0
echo -ne '\E[30;42m'
for HN in `seq 9`; do
echo "#$HN odds->" `cat odds_${HN}`
done
}

# 在起跑線上畫馬.
draw_horses() {
tput cup 6 0
echo -ne '\E[30;42m'
for HN in `seq 9`; do
echo /\\$HN/\\" "
done
} 

print_odds

echo -ne '\E[47m'
# 等待回車按鍵開始賽馬.
# 轉義序列'\E[?25l'禁顯了游標.
tput cup 17 0
echo -e '\E[?25l'Press [enter] key to start the race...
read -s

# 禁用了終端的常規顯示功能.
# 這避免了賽跑時不小心按了按鍵鍵入顯示字元而弄亂了螢幕.
#
stty -echo

# --------------------------------------------------------
# 開始賽跑.

draw_horses
echo -ne '\E[37;47m'
move_and_echo 18 1 $BLANK80
echo -ne '\E[30m'
move_and_echo 18 1 Starting...
sleep 1

# 設定終點線的列數.
WINNING_POS=74

# 記錄賽跑開始的時間.
START_TIME=`date +%s`

# COL 是由下面的"while"結構使用的.
COL=0

while [ $COL -lt $WINNING_POS ]; do

MOVE_HORSE=0

# 檢查 random_1_9 函式是否返回了有效值.
while ! echo $MOVE_HORSE | grep [1-9] &> /dev/null; do
MOVE_HORSE=`random_1_9`
done

# 取得隨機取得的馬的型別和當前位置. 
HORSE_TYPE=`cat horse_${MOVE_HORSE}_position | tail -1`
COL=$(expr `cat horse_${MOVE_HORSE}_position | head -1`)

ADD_POS=1
# 檢查當前的位置是否是障礙物的位置.
if seq 10 7 68 | grep -w $COL &> /dev/null; then
if grep -w $MOVE_HORSE $COL &> /dev/null; then
ADD_POS=0
grep -v -w $MOVE_HORSE $COL > ${COL}_new
rm -f $COL
mv -f ${COL}_new $COL
else ADD_POS=1
fi
else ADD_POS=1
fi
COL=`expr $COL + $ADD_POS`
echo $COL > horse_${MOVE_HORSE}_position # 儲存新位置.

# 選擇要畫的馬的型別.
case $HORSE_TYPE in
1) HORSE_TYPE=2; DRAW_HORSE=draw_horse_two
;;
2) HORSE_TYPE=1; DRAW_HORSE=draw_horse_one
esac
echo $HORSE_TYPE >> horse_${MOVE_HORSE}_position # 儲存當前型別.

# 把前景色設為黑,背景色設為綠.
echo -ne '\E[30;42m'

# 把游標位置移到新的馬的位置.
tput cup `expr $MOVE_HORSE + 5` `cat horse_${MOVE_HORSE}_position |
head -1`

# 畫馬.
$DRAW_HORSE
usleep $USLEEP_ARG

# 當所有的馬都越過 15 行的之後,再次印表機率.
touch fieldline15
if [ $COL = 15 ]; then
echo $MOVE_HORSE >> fieldline15
fi
if [ `wc -l fieldline15 | cut -f1 -d " "` = 9 ]; then
print_odds 
: > fieldline15
fi

# 取得領頭的馬.
HIGHEST_POS=`cat *position | sort -n | tail -1`

# 把背景色重設為白色.
echo -ne '\E[47m'
tput cup 17 0
echo -n Current leader: `grep -w $HIGHEST_POS *position | cut -c7`"
"

done

# 取得賽馬結束的時間.
FINISH_TIME=`date +%s`

# 背景色設為綠色並且啟用閃動的功能.
echo -ne '\E[30;42m'
echo -en '\E[5m'

# 使獲勝的馬閃動.
tput cup `expr $MOVE_HORSE + 5` `cat horse_${MOVE_HORSE}_position | head -1`
$DRAW_HORSE

# 禁用閃動文字.
echo -en '\E[25m'

# 把前景和背景色設為白色.
echo -ne '\E[37;47m'
move_and_echo 18 1 $BLANK80

# 前景色設為黑色.
echo -ne '\E[30m'

# 閃動獲勝的馬.
tput cup 17 0
echo -e "\E[5mWINNER: $MOVE_HORSE\E[25m"" Odds: `cat odds_${MOVE_HORSE}`"\
" Race time: `expr $FINISH_TIME - $START_TIME` secs"

# 恢復游標和最初的顏色.
echo -en "\E[?25h"
echo -en "\E[0m"

# 恢復回顯功能.
stty echo

# 刪除賽跑的臨時檔案.
rm -rf $HORSE_RACE_TMP_DIR

tput cup 19 0

exit 0 

33.6 優化

大多數 shell 指令碼處理不復雜的問題時會有很快的解決辦法。正因為這樣,優化指令碼速度不是一個問題。考慮這樣的情況,一個指令碼處理很重要的任務,雖然它確實執行的很好很正確,但是處理速度太慢。用一種可編譯的語言重寫它可能不是非常好的選擇。最簡單的辦法是重寫使這個指令碼效率低下的部分。這個程式碼優化的原理是否同樣適用於效率低下的 shell 指令碼?

檢查指令碼中的迴圈。反覆執行操作的時間消耗增長非常的快。如果可能,可以從迴圈中刪除時間消耗的操作。

優先使用內建(builtin)命令而不是系統命令。內建命令執行起來更快並且一般呼叫時不會產生新的子 shell。

避免不需要的命令,特別是管道(pipe)。

cat "$file" | grep "$word"

grep "$word" "$file"
# 上面的命令列有同樣的效果,但第二個執行的更有效率,因為它不產生新的子程式.

cat 命令似乎特別常在指令碼中被濫用。
用 time 和 times 工具去了解計算花費的時間。考慮用 C 甚至是彙編重寫關鍵的消耗時間的部分。

嘗試最小化檔案 I/O。Bash 在檔案處理上不是特別地有效率,所以要考慮在指令碼中使用更合適地工具來處理。比如說 awk 或 Perl。

採用結構化的思想來寫指令碼,使各個模組能夠依據需要組織和合並起來。一些適用於高階語言的優化技術也可以用在指令碼上,但有些技術,比如說迴圈優化幾乎是不相關的。上面的討論,依據經驗來判斷。

怎樣優化減少執行時間的優秀指令碼示例,請參考例子 12-42。

33.7 各種小技巧

為了記錄在一個實際的會話期或多個會話期內執行的使用者指令碼,可以加下面的程式碼到每
個你想追蹤記錄的指令碼里。這會記錄下連續的指令碼名記錄和呼叫的次數。

# 新增(>>)下面幾行到你想追蹤記錄的指令碼末尾處.

whoami>> $SAVE_FILE # 記錄呼叫指令碼的使用者.
echo $0>> $SAVE_FILE # 記錄指令碼名.
date>> $SAVE_FILE # 記錄日期和時間.
echo>> $SAVE_FILE # 空行作為分隔行.

# 當然, SAVE_FILE 變數應在~/.bashrc 中定義並匯出(export)。(變數值類似如 ~/.scripts-run)

操作符可以在檔案尾新增內容。如果你想在檔案頭新增內容,那應該怎麼辦?

file=data.txt
title="***This is the title line of data text file***"

echo $title | cat - $file >$file.new
# "cat -" 連線標準輸出的內容和$file 的內容.
# 最後的結果就是生成了一個新檔案,檔案的頭新增了 $title 的值,後跟$file 的內容. 

指令碼也可以像內嵌到另一個 shell 指令碼的普通命令一樣呼叫,如 Tcl 或 wish 指令碼,甚至可以是 Makefile。它們可以作為外部
shell 命令用 C 語言的 system() 函式呼叫,例如system(“script_name”);。

把內嵌的 sed 或 awk 指令碼的內容賦值給一個變數可以增加包裝指令碼(shell wrapper) 的可讀性。參考 例子 A-1 和 例子
11-18。

把你最喜歡和最有用的定義和函式放在一些檔案中。當需要的使用的時候,在指令碼中使用 dot (.) 或 source
命令來"包含(include)"這些"庫檔案"的一個或多個。

#指令碼庫
#本檔案沒有"#!"開頭。也沒有正在做執行動作的程式碼

#有用的變數定義
ROOT_UID=0 # Root 使用者的 $UID 值是 0.
E_NOTROOT=101 # 非 root 使用者出錯程式碼.
MAXRETVAL=255 # 函式最大的的返回值(正值).
SUCCESS=0
FAILURE=-1

#函式
Usage()   # "Usage:" 資訊(即幫助資訊).
{
    if [ -z "$1" ];then
      msg=filename
    else
      msg=$@
    fi
    echo "Usage: `basename $0` "$msg""
}

Check_if_root()   # 檢查是不是 root 在執行指令碼.
{
    if [ "$UID" -ne "$ROOT_UID" ];then
      echo "Must be root to run this script."
      exit $E_NOTROOT
    fi  
}

CreateTempfileName()   # 建立一個"唯一"的臨時檔案.
{
    prefix=temp
    suffix=`eval date +%s`
    Tempfilename=$prefix.$suffix
}

isalph2()   # 測試字串是不是都是字母組成的.
{
    [ $# -eq 1 ] || return $FAILURE
    
    case $1 in
    *[!a-zA-Z]*|"") return $FAILURE;;
    *) return $SUCCESS;;
    esac
}

abs()   # 絕對值.
{
    E_ARGERR=999999
    
    if [ -z "$1" ];then   # 要傳遞引數.
      return $E_ARGERR   # 返回錯誤.
    fi
    
    if [ "$1" -ge 0 ];then   # 如果非負的值,
      absval=$1   # 絕對值是本身.
    else
      let "absval = (( 0 - $1))"   # 否則,改變它的符號.
    fi
    return $absval  
}

tolower()   # 把傳遞的字串轉為小寫
{
    if [ -z "$1" ];then   # 如果沒有傳遞引數,列印錯誤資訊,然後從函式中返回.
      echo "(null)"
      return
    fi
    echo "$@" | tr A-Z a-z
    return 
}

在指令碼中新增特殊種類的註釋開頭標識有助於條理清晰和可讀性。

##表示注意
rm -rf *.zzy  ##"rm"命令的"-rf"組合選項非常的危險,尤其是對萬用字元而言.

#+ 表示繼續上一行.
# 這是第一行
#+ 這是多行的註釋,
#+ 這裡是最後一行.

#* 表示標註
#o表示列表項
#>表示另一個觀點

while [ "$var1" != "end" ]   #> while test "$var1" != "end"

if-test 結構的一種聰明用法是用來註釋一塊程式碼塊。

#!/bin/bash
#
COMMENT_BLOCK=
# 給上面的變數設定某個值就會產生討厭的結果

if [ $COMMENT_BLOCK ];then
Comment block --
=================================
This is a comment line.
This is another comment line.
This is yet another comment line.
=================================

echo "This will not echo."

Comment blocks are error-free! Whee!
fi

echo "No more comments, please."
exit 0

測試$? 退出狀態變數,因為一個指令碼可能想要測試一個引數是否只包含數字,以便後面 可以把它當作一個整數。

#!/bin/bash
#
SUCCESS=0
E_BADINPUT=65

test "$1" -ne 0 -o "$1" -eq 0 2>/dev/null
# 整數要麼等於零要麼不等於零.
# 2>/dev/null 可以抑制錯誤資訊.

if [ $? -ne "$SUCCESS" ];then
  echo "Usage: `basename $0` integer-input"
  exit $E_BADINPUT
fi

let "sum = $1 + 25"   # 如果$1 不是整數就會產生錯誤.
echo "Sum = $sum"
exit 0

0-255
範圍的函式返回值是個嚴格的限制。用全域性變數和其他方法常常出問題。函式內返回值給指令碼主體的另一個辦法是讓函式寫值到標準輸出(通常是用
echo) 作為"返回值",並且將其賦給一個變數。這實際是命令替換(command substitution)的變體。

#!/bin/bash
#
multiply()   # 傳遞乘數.能接受多個引數.
{
    local product=1
    
    until [ -z "$1" ]  # 直到所有引數都處理完畢...
    do
      let "product *= $1"
      shift
    done
    
    echo $product   # 不會列印到標準輸出,因為要把它賦給一個變數.
}
mult1=15383; mult2=25211
val1=`multiply $mult1 $mult2`
echo "$mult1 X $mult2 = $val1"

mult1=25; mult2=5; mult3=20
val2=`multiply $mult1 $mult2 $mult3`
echo "$mult1 X $mult2 X $mult3 = $val2"

mult1=188; mult2=37; mult3=25; mult4=47
val3=`multiply $mult1 $mult2 $mult3 $mult4`
echo "$mult1 X $mult2 X $mult3 X $mult4 = $val3"
exit 0

相同的技術也可用在字串中。這意味著函式可以"返回"一個非數字的值

capitalize_ichar()   # 把傳遞來的引數字串的第一個字母大寫
{
    string0="$@"   # 能接受多個引數.
    
    firstchar=${string0:0:1}   # 第一個字元.
    string1=${string0:1}   # 餘下的字元.
    
    FirstChar=`echo "$firstchar" | tr a-z A-Z`   # 第一個字元轉換成大寫字元.
    
    echo "$FirstChar$string1"
}
newstring=`capitalize_ichar`
echo "$newstring"

用這個辦法甚至可能"返回"多個值

Example 33-16 整型還是string?
#!/bin/bash
#
sum_and_product()   # 計算所傳引數的總和與乘積.
{
    echo $(($1 + $2)) $(($1 * $2))   # 列印每個計算的值,用空格分隔開.
}
echo
echo "輸入第一個數字"
read first
echo
echo "輸入第二個數字"
read second
echo

retval=`sum_and_product $first $second`   # 把函式的輸出賦值給變數.
sum=`echo "$retval" | awk '{print $1}'`   # 把第一個域的值賦給 sum 變數.
product=`echo "$retval" | awk '{print $2}'`   # 把第二個域的值賦給 product 變數.

echo "$first + $second = $sum"
echo "$first * $second = $product"
echo
exit 0

傳遞陣列給函式,然後"返回"一個陣列給指令碼

用變數替換(command substitution)把陣列的所有元素用空格分隔開來並賦給一個變數 就可以實現給函式傳遞陣列。用先前介紹的方法函式內 echo 一個陣列並"返回此值",然後呼叫命令替換用 ( … ) 操作符賦值給一個陣列。

Example 33-17 傳遞和返回陣列

#!/bin/bash
#
Pass_Array()
{
    local passed_array   # 區域性變數.
    passed_array=(`echo "$1"`)
    echo "${passed_array[@]}"
}
original_array=( element1 element2 element3 element4 element5 )
echo
echo "original_array = ${original_array[@]}"

#下面是傳遞陣列給函式的技巧
argument=`echo ${original_array[@]}`
#把原陣列的所有元素用空格分隔開合成一個字串並賦給一個變數
#注意:只是把陣列本身傳給函式是不會工作的.

#下面是允許陣列作為"返回值"的技巧.
returned_array=(`Pass_Array "$argument"`)
#把函式的輸出賦給陣列變數.

echo "returned_array = ${returned_array[@]}"

#嘗試在函式外存取(列出)陣列.
Pass_Array "$argument"

#函式本身可以列出陣列,但...函式外存取陣列被禁止.
echo "Passed array (within function) = ${passed_array[@]}"
## 因為變數是函式內的區域性變數,所以只有 NULL 值.
echo
exit 0

利用雙括號結構,使在for和while迴圈中可以使用C風格的語法來設定和增加變數。參考例子10-12和例子10-17

在指令碼開頭設定path和umask增加指令碼的"可移植性"–在某些把$PATH和umask弄亂的系統裡也可以執行

#!/bin/bash
#
PATH=/bin:/usr/bin:/usr/local/bin ; export PATH
umask 022   # 指令碼的建立的檔案有 755 的許可權設定.

一個有用的指令碼技術是:重複地把一個過濾器的輸出回饋(用管道)給另一個相同過濾器,但過濾器有不同的引數和選項。尤其對tr和grep更合適。

wlist=`string "$1" | tr A-Z a-z | tr '[:space:] Z' | tr -cs '[:alpha:]' Z | tr -s '\173-\377' Z | tr Z ' '`

Example 33-18 anagrams遊戲

#!/bin/bash
#
LETTERSET="eta...oinanagrams...shrdanagrams...luanagrams"
FILTER='.......'
echo "$LETTERSET" |
grep "$FILTER" |   # 至少 7 個字元,
grep '^is' |   # 以'is'開頭
grep -v 's$' |   # 不是複數的(指英文單詞複數)
grep -v 'ed$'   # 不是過去式的(當然也是英文單詞)
exit 0

使用"匿名的 here documents" 來註釋程式碼塊,這樣避免了對程式碼塊的每一塊單獨用#來註釋了。參考例子 17-11

當依賴某個命令指令碼在一臺沒有安裝該命令的機器上執行時會出錯。使用 whatis 命令可以避免此問題。

CMD=command1
PlanB=command2

command_test=$(whatis "$CMD" | grep 'nothing appropriate')
# 如果'command1'沒有在系統裡發現 , 'whatis'會返回:"command1: nothing appropriate."

if [[ -z "$command_test" ]];then   # 檢查命令是否存在.
  $CMD option1 option2
else
  $PlanB
fi    

在發生錯誤的情況下if-grep test 可能不會返回期望的結果,因為文字是列印在標準出錯而不是標準輸出上。

if ls -l nonexistent_filename | grep -q 'No such file or directory'
then
  echo "File \"nonexistent_filename\" does not exist."
fi  

把標準出錯重定向到標準輸出上可以修改這個

if ls -l nonexistent_filename 2>&1 | grep -q 'No such file or directory'
then
  echo "File \"nonexistent_filename\" does not exist."
fi  

在 shell 指令碼里能呼叫 X-Windows 的視窗小部件將多麼美好。已經存在有幾種工具包實現這個了,它們稱為
Xscript、Xmenu 和 widtools。頭兩個已經不再維護。

注意: widtools (widget tools) 工具包要求安裝了 XForms 庫。另外,它的 Makefile 在典型的 Linux 系統上安裝前需要做一些合適的編輯。最後,提供的 6 個部件有 3 個 不能工作 (事實上會發生段錯誤)。

dialog 工具集提供了 shell 指令碼使用一種稱為"對話方塊"的視窗部件。原始的 dialog 軟體包工作在文字模式的控制檯下,但它的後續軟體 gdialog、Xdialog 和 kdialog 使用基於 X-Windows 的視窗小部件集。

Example 33-19 在shell指令碼中呼叫的視窗部件

#!/bin/bash
#
#必須在你的系統裡安裝'dialog'才能執行此指令碼.

# 在視窗中的輸入錯誤.
E_INPUT=65
# 輸入視窗顯示的尺寸.
HEIGHT=50
WIDTH=60

# 輸出檔名 (由指令碼名構建而來).
OUTFILE=$0.output

# 把這個指令碼的內容顯示在視窗中.
dialog --title "Displaying: $0" --textbox $0 $HEIGHT $WIDTH

# 現在,儲存輸入到輸出檔案中.
echo -n "VARIABLE=" > $OUTFILE
dialog --title "User Input" --inputbox "Enter variable, please:" $HEIGHT $WIDTH 2>> $OUTFILE

if [ "$?" -eq 0 ];then
  echo "Executed \"dialog box\" without errors."
else
  echo "Error(s) in \"dialog box\" execution."
  rm $OUTFILE
  exit $E_INPUT
fi

#  現在,我們重新取得並顯示儲存的變數.
. $OUTFILE # 'Source' 儲存的檔案(即執行).
echo "The variable input in the \"input box\" was: "$VARIABLE""

rm $OUTFILE # 清除臨時檔案.
exit $?

其他的在指令碼中使用視窗的工具還有 Tk 或 wish (Tcl 派生物),PerlTk (Perl 的 Tk 擴充套件),tksh (ksh 的 Tk 擴充套件),XForms4Perl (Perl 的 XForms 擴充套件),Gtk-Perl (Perl 的 Gtk 擴充套件)或 PyQt (Python 的 Qt 擴充套件)。

為了對複雜的指令碼做多次的版本修訂管理,可以使用 rcs 軟體包。

使用這個軟體包的好處之一是會自動地升級 ID 頭標識。在 rcs 的 co 命令處理一些預定義的關鍵字引數替換。例如:代替指令碼里頭#$Id$的,如類似下面的行:

#$Id: hello-world.sh,v 1.1 2004/10/16 02:43:05 bozo Exp $

33.8 安全話題

33.8.1 被感染的指令碼

有一個簡短的關於指令碼安全的介紹是適當的。指令碼程式可能會包含蠕蟲病毒、特洛伊木馬或是其他的病毒。由於這些原因,決不要以 root 身份執行指令碼 (或允許它被插入到系統的 /etc/rc.d 裡的啟動指令碼中) 除非你確定這是值得信賴的原始碼或你已經很小心地分析過了指令碼並確信它不會有什麼危害。

33.8.2 隱藏Shell指令碼原始碼

為了安全,使指令碼不可讀是有必要的。如果有軟體可以把指令碼轉化成相應的二進位制執行檔案就好了。Francisco Rosales 的 shc - 通用的 Shell 指令碼編譯器(generic shell script compiler) 可以出色地完成目標。

33.9 移植話題

以現在的情況來看,許多種 shell 和指令碼語言都盡力使自己符合 POSIX 1003.2 標準。用 --posix 選項呼叫 Bash 或在指令碼開頭插入 set -o posix 就能使 Bash 能以很接近這個標準的方式執行。在指令碼開頭用"#!/bin/sh" 比用 "#!/bin/bash"會更好。
注意:在 Linux 和一些 UNIX 風格的系統裡/bin/sh 是/bin/bash 的一個連結(link),並且如果指令碼以/bin/sh 呼叫時會禁用 Bash 的擴充套件功能。

33.10 在Windows下進行shell程式設計

使用其他作業系統使用者希望能執行 UNIX 型別的指令碼能在他們的系統上執行,因此也希望能在這本書裡能學到這方面的知識。來自 Cygnus 的 Cygwin 軟體結合來自 Mortice Kern 的 MKS 軟體包(MKS utilities)可以給 Windows 新增 shell 指令碼的相容。

相關文章