ANSI[1]定义了屏幕属性的转义序列集合, 比如说粗体文本, 前景与背景颜色. DOS批处理文件通常都使用ANSI转义码来控制 颜色输出, Bash脚本也是这么做的.
例子 33-11. 一个"彩色的"地址数据库
#!/bin/bash # ex30a.sh: 脚本ex30.sh的"彩色"版本. # 没被加工处理过的地址数据库 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 "(Enter only the first letter of name.)" echo echo -en '\E[47;34m'"\033[1mE\033[0m" # 蓝色 tput sgr0 # 将颜色重置为"常规". echo "vans, Roland" # "[E]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" ;; # 稍后为Smith & Zane添加信息. * ) # 默认选项. # 空输入(直接按回车)也会在这被匹配. echo echo "Not yet in database." ;; esac tput sgr0 # 将颜色重置为"常规". echo exit 0 |
例子 33-12. 画一个盒子
#!/bin/bash # Draw-box.sh: 使用ASCII字符画一个盒子. # 由Stefano Palmeri编写, 本书作者做了少量修改. # 经过授权, 可以在本书中使用. ###################################################################### ### 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, #+ 不会有消息打印到stderr上. # # 开始画盒子之前, 会清屏. # 函数内不包含清屏命令. # 这样就允许用户画多个盒子, 甚至可以叠加多个盒子. ### 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" # 如果定义了, 就设置盒子边框的颜色. # 开始画盒子 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"表示转义序列结束.
bash$ echo -e "\033[1mThis is bold text.\033[0m" |
一种类似的转义序列用来切换下划线属性(在rxvt 和aterm上).
bash$ echo -e "\033[4mThis is underlined text.\033[0m" |
echo命令的 |
其他的转义序列可用于修改文本和背景色.
bash$ echo -e '\E[34;47mThis prints in blue.'; tput sgr0 bash$ echo -e '\E[33;44m'"yellow text on blue background"; tput sgr0 bash$ echo -e '\E[1;33;44m'"BOLD yellow text on blue background"; tput sgr0 |
通常情况下, 为浅色的前景文本设置粗体属性比较好. |
tput sgr0把终端设置恢复为原样. 如果省略这一句, 那么这个终端所有后续的输出还会是蓝色.
因为tput sgr0在某些环境下不能恢复终端设置, echo -ne \E[0m可能是更好的选择. |
可以在有色的背景上, 使用下面的模板, 在上面写有色的文本. echo -e '\E[COLOR1;COLOR2mSome text goes here.' "\E["开始转义序列. 以分号分隔的数字"COLOR1"和"COLOR2"分别指定了前景色和背景色, 数值与色彩之间的对应, 请参考下面的表格. (数值的顺序其实没关系, 因为前景色和背景色的数值都落在互不重叠的范围中.) "m"用来终止转义序列, 文本紧跟在"m"的后面. 也要注意, 单引号将echo -e后面的命令序列都引用了起来. |
下表的数值是在rxvt终端上运行的结果. 具体的结果可能和在其他终端上运行的结果不同.
例子 33-13. 显示彩色文本
#!/bin/bash # color-echo.sh: 使用颜色来显示文本消息. # 可以按照你自己的目的来修改这个脚本. # 这比将颜色数值写死更容易. 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 () # Color-echo. # 参数$1 = 要显示的信息 # 参数$2 = 颜色 { local default_msg="No message passed." # 其实并不一定非的是局部变量. message=${1:-$default_msg} # 默认为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参数. # ---------------------------------------------------- echo exit 0 # 练习: # ----- # 1) 为'cecho ()'函数添加"粗体"属性. # 2) 为彩色背景添加选项. |
例子 33-14. "赛马"游戏
#!/bin/bash # horserace.sh: 一个非常简单的模拟赛马的游戏. # 作者: Stefano Palmeri # 经过授权可以在本书中使用. ################################################################ # 脚本目的: # 使用转义序列和终端颜色进行游戏. # # 练习: # 编辑脚本, 使它运行起来更具随机性, #+ 建立一个假的赌场 . . . # 嗯 . . . 嗯 . . . 这种开场让我联想起一部电影 . . . # # 脚本将会给每匹马分配一个随机障碍. # 按照马的障碍来计算几率, #+ 并且使用一种欧洲(?)的风格表达出来. # 比如: 几率(odds)=3.75的话, 那就意味着如果你押$1, #+ 你就会赢得$3.75. # # 此脚本已经在GNU/Linux操作系统上测试过, #+ 测试终端有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 # 如果脚本被Ctl-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 # 画6条白线. 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 # 禁用了终端的常规echo功能. # 这么做用来避免在比赛中, #+ 按键所导致的"花"屏. 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` # 下面的"while"结构需要使用COL变量. 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 |
也请参考例子 A-22.
然而, 这里有一个严重的问题. ANSI转义序列是不可移植的. 在某些终端(或控制台)上运行的好好的代码, 可能在其他终端上根本没办法运行. "彩色"的脚本可能会在脚本作者的机器上运行的非常好, 但是在其他人的机器上就可能产生不可读的输出. 因为这个原因, 使得"彩色"脚本的用途大打折扣, 而且很有可能使得这项技术变成华而不实的小花招, 甚至成为一个"玩具". |
Moshe Jacobson的彩色工具(http://runslinux.net/projects.html#color)能够非常容易的简化ANSI转义序列的使用. 这个工具使用清晰而且富有逻辑的语法代替了之前讨论的难用的结构.
Henry/teikedvl也开发了一个类似的工具(http://scriptechocolor.sourceforge.net/)用来简化彩色脚本的创建.
[1] | 当然, ANSI是美国国家标准组织(American National Standards Institute)的缩写. 这个令人敬畏的组织建立和维护着许多技术和工业的标准. |