Linux 版 (精华区)
作 家: Beth (打网球去!) on board 'unix'
题 目: FAQ2
来 源: 哈尔滨紫丁香站
日 期: Sat Apr 12 08:42:26 1997
出 处: qlu@hitcrc.hitcrc.hit.edu.cn
发信人: Only (在水一方), 信区: Unix
标 题: UNIX chinese FAQ(2)
发信站: 饮水思源 (Sun Nov 17 10:51:16 1996)
本篇文章回答以下问题:
2.1) 我要怎么删除以 '-' 字元开头为档名的档案?
2.2) 我要怎样才能把档名当中含有特殊字元的档案删除?
2.3) 我要如何列出整个目录树呢?
2.4) 要怎么设定 prompt 才会显示出目前所在的目录?
2.5) 当我在写 shell script 时,要如何从 terminal 读入字元?
2.6) 怎么样把 "*.foo" 改名为 "*.bar" 呢?怎样把档案名称改成小写呢?
2.7) 为什么我用 "rsh host command" 会有一些奇怪的讯息出现?
2.8) 我要怎要用程式或者是 shell script 中设定目前所用的 shell 的环境
2.9) 我要如何将 csh 的 stdout 与 stderr 导向到不同的胤侥兀
2.10) 我如何在 .cshrc 中判断是否在 login shell 中?
2.11) 在 shell 中要用怎样的 pattern 来表示除了 "." 与 ".." 外的所有档
2.12) 在 Bourne shell script 里要怎么找出最后一个参数?
2.13) 为什么有人说 $PATH 里不可以放 '.' 呢?
2.14) 在 shell script 中要怎么让终端机发出声音呢?
2.15) 为什么我不能用 "talk" 与我在某机器上的朋友交谈呢?
2.16) 为什么我月历是错的?
2.1) 我要怎么删除以 '-' 字元开头为档名的档案?
找一个方法让档案名称开头不要是 '-' 就可以了,最简单的方法就是使用
rm ./-filename
(当然,我们假设 "-filename" 位于目前的目录)。这个方法可以避免让其
他指令解释 "-"。
有许多指令,特别是呼叫 "getopt(3)" 的参数剖析常式的程式,会接受一
个 "--" 的参数,代表「这是最后一个选项」,此后出现的项目都不再是选
项,因此您的 rm 可能会接受这个 "rm -- -filename" 这种写法。有些不用
getopt() 的 rm 程式也会以同样的方式处理单一字元 "-",因此您也可以试
试 "rm - -filename"。
2.2) 我要怎样才能把档名当中含有特殊字元的档案境
如果这个「特殊字元」是 '/',请跳到这题的结尾;如果这个特殊的字元是
一个 ' 或者控制字元或者中文字,请继续往下读。
典型的解法是:
rm -i some*pattern*that*matches*only*the*file*you*want
这样子的话 rm 会在要删除符合你给的条件的档案前,要你确定,不
过若你的 shell 会将每个字元的第八个 bit 变成零,那以中文作档
名的档案可能就删除不掉了!
与
rm -ri .
这样子的 rm 会删除目前目录下的所有档案,而在删除一个档案之
前会问你是否要删除此档。不过很不幸的,并非每一个版本的 rm 都
能这么用。再者,就算能用的话,这么做的话会把目前所在目录的所
有子目录都找进去,可能要用 "chmod a-x" 避免使子目录无法搜寻才
能避免可怕的后果。要做 "rm -r" 或含有万用字元的 "rm" 前请先深
呼吸,搞清楚自己是在做什么!
与
find . -type f ... -ok rm '{}' \;
"..." 是一堆用以辨识档案名称的述词,譬如在找出一有问的档案的
inode 为何后,用
find . -num 12345 -ok rm '{}' \;
或
find . -inum 12345 -ok mv '{}' new-file-name \;
删除或改名。 选项 "-ok" 是告诉 find 要执行指令前先要求你确认
。若你能确定所下的指令没有问题,或者怕所要处理档案有奇怪的字
元印出来会使萤幕乱七八糟,那用选项 "-exec" 就不会先要求你的确
认。
那当档案名称里含有 '/' 时要怎么办呢?
这类档案是很特别的情形,并且只会因为 kernel 的 bug 而发生(通
常是在写 NFS 的时候,没有把从远端机器来的档案名称中不合规定的
字元过滤掉)。我们第一件要做的事情就是,试著去了解为什么这个
问题会如此奇怪。
UNIX 的目录其实就只是单纯的档名和 inode number 的成对组合。
举例来说,目录包含了如下的资讯:
filename inode
file1 12345
file2.c 12349
file3 12347
理论上挥 '/' 和 '\0' 两个字元不能用在档案名称中,
因为它们有以下的特殊用途:
'/' :用来分隔目录名称及档案名称。
'\0' :用来当档名的终结字元。
非常、极端、很不幸的,某些厂商做出来的 NFS 在回应远端机器的要
求时,会很白痴地造出含有斜线(/)的档名。例如,当某人在 Mac
或其他非 Unix 机器透过 NFS 造一个以日期为名称的档案到你的
Unix 中。那么,你的 Unix 目录看起来可能就会像这个样子:
filename inode
91/02/07 12357
我们前面所提过的 'find' 或 'rm' 都无法删除这个档案,因为这些或
其他的 Unix 程式都会强制把 '/' 当作前述的分隔字元解释。
其实,任何一般的程式都会试著做 unlink("91/02/07"),而这对
kernel 来说,它的意义是 "unlink 目录 91 下的子目录 02 中的档
案 07",但是,我们并没有这样的档案,我们有的是一个名叫
"91/02/07" 的档案在目前的目录中。这是个极细微但极重要的区别。
这时该怎么办呢?首先回到产生这种乱七八糟档名的 Mac,试试
看 NFS daemon 要不要让你改成不含 '/' 的档名。如果不行,那就得
找你的系统管理者帮忙了。请他试试以下几种方法之一:
1. 用 "ls -i" 找出档案的 inode number,umount 掉这个
file system 然后以 "clri" 将这个 inode 清除,然后
祈求“fsck" 的成功。这个作法会删除这乱七八糟档名的
档案。
2. 若还想保存这个档案的资料,试试以下的做法:
-在那乱七八糟档名的档案所在之目录的亲目录底下建一
个子目录,将旧的目录下能搬动的档案都搬到新的目录
里。
-以 "ls -id" 取得旧目录的 inode number
-unmount 掉这个 file system, 用 "clri" 清掉那个
目录的 inode
-"fsck" 那个 file system
-从新 mount 上那个 file system
-将新的目录改名为旧的目录名
-从 lost+found 下找回那个档案,改个好名字,放回原
来的目录。
3.若你有一个叫做 "fsdb" 的程式,那你可以试试看喽!
2.3) 我要如何列出整个目录树呢?
底下有几种做法自己挑一个吧:
ls -R (not all versions of "ls" have -R)
find . -print (should work everywhere)
du -a . (shows you both the name and size)
若你要找的是特定的档案,例如说是档名结尾为 ".c" 者,可用
find . -name '*.c' -print
"find" 是一个很强很好用的程式。值得一学。
2.4) 要怎么设定 prompt 才会显示出目前所在的目录?
这得视你的 shell 而定。有些 shell 很容易,有些 shell 很难,有些根
本办不到。
C Shell (csh):
将以下的东西加入你的 .cshrc 里。
alias setprompt 'set prompt="${cwd}% "'
setprompt # to set the initial prompt
alias cd 'chdir \!* && setprompt'
假如你有用 pushd 与 popd, 把底下的东西也加进去。
alias pushd 'pushd \!* && setprompt'
alias popd 'popd \!* && setprompt'
若你的 C shell 没有 $cwd 这个变数,那就得用 `pwd` 代替之。
若你想要的只是 prompt 里有目前所在目录的最后一个成分
("mail%" 而 "/usr/spool/mail%") 则用
alias setprompt 'set prompt="$cwd:t% "'
有些旧版的 csh 将 && 和 || 的意义弄反了。你可以试试看:
false && echo bug
若结果是印出 "bug",那就把 && 和 || 对调,或找一个没有这种
bug 的 csh 来用。
Bourn Shell (sh):
如果你有较新版的 Bourn Shell(SVR2 或更新的版本),那么你就可
以用一个 shell function 来造你自己的命令,譬如 "xcd":
xcd() { cd $* ; PS1="`pwd` $ ";}
如果你的 Bourn Shell 是比较旧的版本,也是可以做到,但是方法比
较复杂。这里提供一个方法。把以下的内容加入你的 .profile:
LOGIN_SHELL=$$ export LOGIN_SHELL
CMDFILE=/tmp/cd.$$ export CMDFILE
# 16 is SIGURG, pick a signal that's not likely to be
used
PROMPTSIG=16 export PROMPTSIG
trap '. $CMDFILE' $PROMPTSIG
然后把以下的部份写成一个可执行的 script(不需要缩排),名字就
叫做 "xcd",放在你的 PATH 中
: xcd directory - change directory and set prompt
: by signalling the login shell to read a command file
cat >${CMDFILE?"not set"} <<EOF
cd $1
PS1="\`pwd\`$"
EOF
kill -${PROMPTSIG?"not set"} ${LOGIN_SHELL?"not set"}
那么,现在就可已用 "xcd /some/dir" 来改变工作目录了。
Korn Shell (ksh):
把下面这行加入你的 .profile 中:
PS1='$PWD $ '
如果你只想显示最后一个部分,那么就用
PS1='${PWD##*/} $ '
T C shell (tcsh)
Tcsh 是常用的 csh 加强版,增加了一些内建变数(及许多其他的功
能):
%~ the current directory, using ~ for $HOME
%/ the full pathname of the current directory
%c or %. the trailing component of the current directory
所以你可以直接用
set prompt='%~'
BASH (FSF's "Bourne Again Shell")
$PS1 中的 \w 表示工作目录的完整路径(以 ~ 表示 $HOME);\W 则
是表示工作目录的最后一个部份。所以,只要把前面所提有关 sh 和
ksh 的方法做以下的修改
PS1='\w $ '
或
PS1='\W $ '
2.5) 当我在写 shell script 时,要如何从 terminal 读入字元?
在 sh 中,你可以用 read。通常是使用在回圈,如下例:
while read line
do
...
done
在 csh 中,则用 $<:
while ( 1 )
set line = "$<"
if ( "$line" == "" ) break
...
end
很可惜的,csh 并没有方法判断空白行和档案结尾(end-of-file)的不同。
如果你要用 sh 从 terminal 读一个字元,那么你可以试试
echo -n "Enter a character: "
stty cbreak # or stty raw
readchar=`dd if=/dev/tty bs=1 count=1 2>/dev/null`
stty -cbreak
echo "Thank you for typing a $readchar ."
2.6) 怎么样把 "*.foo" 改名为 "*.bar" 呢?怎样把档案名称改成小写呢?
为什么 "mv *.foo *.bar" 不对呢? 想想 shell 是怎样把万用字元展开的
吧。 在 mv 读取参数前 "*.foo" 与 "*.bar" 就已经展开了。"mv *.foo
*.bar" 在各种不同的 shell 会有不同的结果。 Csh 的话会印出 "No
match",因为找不到 "*.bar"。 Sh 则是会执行 "mv a.foo b.foo c.foo
*.bar",也就是说如果只有在有一名为 "*.bar" 的子目录存在时 mv 才会
认为执行成功,不过就算成功也不是你所预期的结果。
正确的做法是用你所用的 shell 提供的回圈来做。若你的系统中有
"basename" 这个指令:
C Shell:
foreach f ( *.foo )
set base=`basename $f .foo`
mv $f $base.bar
end
Bourne Shell:
for f in *.foo; do
base=`basename $f .foo`
mv $f $base.bar
done
有些 shell 会提供就自己的变数替代功能,那就可以不用 "basename",
而用更简单的回圈了:
C Shell:
foreach f ( *.foo )
mv $f $f:r.bar
end
Korn Shell:
for f in *.foo; do
mv $f ${f%foo}bar
done
如果没有 "basename" 可用或是胍鱿癜 foo.* 改名为 bar.* 之类的
事,那么可以用其他的方法如 "sed" 把原来的档案名称做分隔的动作,但
是回圈的想法是一样的。你也可以利用 "sed" 把档名转换成 "mv" 的命令
,然后再把这些命令转给 "sh" 执行。如下:
ls -d *.foo | sed -e 's/.*/mv & &/' -e 's/foo$/bar/' | sh
在 1990 年 4 月时,Vladimir Lanin 把他自己写的一个叫 "mmv" 的程式
post 到 comp.sources.unix (Volumn 21, issues 87 and 88),这个程式
就能够把这件事处理得很好。 你可以这样使用:
mmv '*.foo' '=1.bar'
以上所提的 shell 中的回圈也可以用来做档案名称的大、小写转换。你可
以用改档名的方式把大写档名改为小写:
C Shell:
foreach f ( * )
mv $f `echo $f | tr '[A-Z]' '[a-z]'`
end
Bourne Shell:
for f in *; do
mv $f `echo $f | tr '[A-Z]' '[a-z]'`
done
Korn Shell:
typeset -l l
for f in *; do
l="$f"
mv $f $l
done
如果你还希望能处理含有特殊字元(空白或其他的奇怪字元)的档名,那
么你最好用:
Bourne Shell:
for f in *; do
g=`expr "xxx$f" : 'xxx\(.*\)' | tr '[A-Z]' '[a-z]'`
mv "$f" "$g"
done
'expr' 不管档名里有没有特殊字元都会印出档名。
有些版本的 "tr" 需要用 '[' 和 ']',有些则不必。不过,不管是不是一
定要用 '[' 与 ']' 的 "tr",加了总是没有害处。
若你的系统里有装 "perl",那你可以用 Larry Wall 写的这个多用途改档
名的程式。
#!/usr/bin/perl
#
# rename script examples from lwall:
# rename 's/\.orig$//' *.orig
# rename 'y/A-Z/a-z/ unless /^Make/' *
# rename '$_ .= ".bad"' *.f
# rename 'print "$_: "; s/foo/bar/ if <stdin> =~ /^y/I' *
$op = shift;
for (@ARGV) {
$was = $_;
eval $op;
die $@ if $@;
rename($was,$_) unless $was eq $_;
}
2.7) 为什么我用 "rsh host command" 会有一些奇怪的讯息出现?
(这里所指的 "rsh"[也可能是 "remsh" 或 "remote"] 是 remote shell,
而不是在有些系统中名为 "rsh" 的 restricted shell,这两者天差地远
了!)
若你在远端的帐号用的是 C shell,那远端的主机会帮你启动一个 C
shell 来完成你所下的那个 'command',这个 shell 会读取你在远端的
.cshrc 档。若你的 .cshrc 中有 "stty" 或 "biff" 这类不适合 non-
interactive shell 的指令。那就可能会有你所意想不到的结果,举例来
说,若你把
stty erase ^H
biff y
放在你的 .cshrc 档里面你可能会得到类似以下的奇怪讯息
% rsh some-machine date
stty: : Can't assign requested address
Where are you?
Tue Oct 1 09:24:45 EST 1991
若你使用 "at" 或 "cron",那可能也会得到类似的错误讯息。
不过没关系,解决的方法非常简单。若你的 ".cshrc" 里面有一堆只有在
interactive shell 中才有用的 operation,那就将那些 operation 都用
以下的做法包起来:
if ( $?prompt ) then
operations....
endif
因为在一个 non-interactive 中不应该也没有必要去设定 "prompt"。
还有一些只有在开启一个 login session时才有用的东西,最好搬到
".login" 中去。
2.8) 我要怎要用程式或者是 shell script 中设定目前所用的 shell 的环境
变数或改变所在的目录?
若没有做一些特殊安排是做不到的。因为,当我们造出一 child process
时,此 process 会继承其 parent 的变数与所在的目录。在这个 child
process 只能改到自己的变数与所在目录而无法影响到其 parent。
要达到此目的,parent process 要与 child process 有所沟通。当
child process 要改变变数值时得把要改变的变数及其内容写到一个讲好
的地方,让 parent process 去读取, 并改变 parent process 的变数。
另一个做法则是写一个 shell script,然后在 Bourne shell 或 Korn
shell 中用 ".",在 C shell 中用 source 去执行那个 shell script。
若此 sript 名为 "myscript" :
在 Bourne shell 或 Korn shell 中就用
. myscript
在 C shell 中则用
source myscript
若你想做的只是要改变所在目录或是设定一个环境变数,那使用 C shell
中的 alias 或是 Bourne/Korn shell 中的函数就可达成你的目的。可参
考"要怎么设定 prompt 才会显示出目前所在的目录"一节中的做法。
Thomas Michanek (xtm@telelogic.se) 提供一个更详细的解答(
ftp://ftp.wg.omron.co.jp/pub/unix-faq/docs/script-vs-env)。
2.9) 我要如何将 csh 的 stdout 与 stderr 导向到不同的地方呢?
在 csh 中,用 ">" 将 stdout 导向,用 ">&" 则能将 stdout 与 stderr
一起导向。可是不能只单独把 stderr 转向。最好的方法是
( command >stdout_file ) >&stderr_file
以上的命令会开一个 subshell 执行 "command";而这个 subshell 的
stdout 则被转向到 stdout_file,同时这个 subshell 的 stdout 和
stderr 则都被转向到 stderr_file,但是因为 stdour 已经先被转向了,
所以 stderr 就会被转到 stderr_file 了。
如果你只是单纯的不想把 stdout 做转向,那么就用 sh 来帮你吧。
sh -c 'command 2>stderr_file'
2.10) 我如何在 .cshrc 中判断是否在 login shell 中?
当有人这么问的时候,通常要问的是
要如何判断是否是一个 interactive shell? 或是问要如何判断是否是
最上层的 shell ?
若你是要问 "是否在 login shell 中"(注:就是在做完 .cshrc 后,会
再去做 .login),那么你也许用 "ps" 和 "$$" 随便弄一弄,就能知道了
。因为通常 login shells 的名字在 "ps" 看起来都是由 '-' 做开头的。
如果你是真的对另外两个问题感兴趣,那么这里有个方法可以让你在
.cshrc 中判断。
if (! $?CSHLEVEL) then
#
# This is a "top-level" shell,
# perhaps a login shell, perhaps a shell started up by
# 'rsh machine some-command'
# This is where we should set PATH and anything else we
# want to apply to every one of our shells.
#
setenv CSHLEVEL 0
set home = ~username # just to be sure
source ~/.env # environment stuff we always want
else
#
# This shell is a child of one of our other shells so
# we don't need to set all the environment variables again.
#
set tmp = $CSHLEVEL
@ tmp++
setenv CSHLEVEL $tmp
endif
# Exit from .cshrc if not interactive, e.g. under rsh
if (! $?prompt) exit
# Here we could set the prompt or aliases that would be useful
# for interactive shells only.
source ~/.aliases
2.11) 在 shell 中要用怎样的 pattern 来表示除了 "." 与 ".." 外的所有档案?
这个问题看来容易。因为你可以用
* 表示所有不是以 "." 为开端的档案
.* 表示所有以 "." 为开端的档案,但是这样会把 "." 和 ".." 也包
含进来,但是通常你并不会想把这两个也含进来。
.[!.]* 这只有比较新的 shells 才能用;某些 shells 用 "^" 代替 "!";
而符合 POSIX 标准的 shells 一定苡 "!",但是大部份也都能接
受 "^";所有具可移植性的应用程式都不应该在 "[" 之后紧接著没被
quota 起来的 "^")表示所有以 "." 为开头并且第二个字元不是 "."
的档案;但是这样却会漏掉 "..foo" 这类的档案。
.??* 表示所有以 "." 为开头且档名长度至少为 3 的档案,这样大概
就能避开 "." 和 ".." 了,但是却还是会漏掉 ".a" 这类的档。
所以想要正确地表示除了 "." 与 ".." 之外所有的档案,你必须要用到 3
个 patterns(如果你没有像 ".a" 这样的档案,那你可以去掉第一个
pattern):
.[!.]* .??* *
或者你也可以用一两个外部程式和 backquote substitution。这样就很完
美了:
`ls -a | sed -e '/^\.$/d' -e '/^\.\.$/d'`
(or `ls -A` in some Unix versions)
不过即使是这样做,碰上档名里面含有换行字元, IFS 字元,或是万用字
元仍然是没辄。
2.12) 在 Bourne shell script 里要怎么找出最后一个参数?
由
Martin Weitzel <@mikros.systemware.de:martin@mwtech.uucp>
Maarten Litmaath <maart@nat.vu.nl>
提供的答案:
若你能确定参数不会超过九个的话,可用:
eval last=\${$#}
在符合 POSIX 标准的 shell 里,不管有多少个参数都可用上述的方法。
底下方法是一定有用的:
for last
do
:
done
更一般性的做法是:
for i
do
third_last=$second_last
second_last=$last
last=$i
done
若你想做的是将最后一个参数去除或是将一堆参数的顺序反过来或是取用
第 N 个参数。底下是一个不用造出 subprocess 只用 shell 组建功能的
做法:
t0= u0= rest='1 2 3 4 5 6 7 8 9' argv=
for h in '' $rest
do
for t in "$t0" $rest
do
for u in $u0 $rest
do
case $# in
0)
break 3
esac
eval argv$h$t$u=\$1
argv="$argv \"\$argv$h$t$u\"" # (1)
shift
done
u0=0
done
t0=0
done
# now restore the arguments
eval set x "$argv" # (2)
shift
这个例子可以用到 999 个参数,应该够用了吧?仔细看看(1)与(2)标示的
地方,想办法说服你自己不管参数里面有什么奇怪的字元这两行都不会出
差错。
要找第 N 个参数,用:
eval argN=\$argv$N
要将参数的顺序反过来,标示为(1)的那一行必须改成
argv="\"\$argv$h$t$u\" $argv"
自己练习最后一个参数去除的方法。
若允许呼叫外部指令这类造出 subprocess 的做法,代志就更好办了。
底下是找出 argvN:
N=1
for i
do
eval argv$N=\$i
N=`expr $N + 1`
done
要将参数的顺序反过来还有一个不用造出 subprocess,有更简单的方法。
这个方法也可以用来去除最后一个参数, 不过要注意的是 argvN 不在是
原来的第 N 个参数:
argv=
for i
do
eval argv$#=\$i
argv="\"\$argv$#\" $argv"
shift
done
eval set x "$argv"
shift
2.13) 为什么有人说 $PATH 里不可以放 '.' 呢?
背景知识: 环境变数 PATH 是一串用冒号隔开的目录。当你下一个指令而
没有指定所在的目录,例如 "ls" 而非 "/bin/ls",则你的 shell 就会在
PATH 所指定的目录中去找寻指令。
你可以在 PATH 里面放入目前所在的目录 "." 。或者,在 PATH 中加入一
个空的目录,这两者是等效的
for csh users:
setenv PATH :/usr/ucb:/bin:/usr/bin
setenv PATH .:/usr/ucb:/bin:/usr/bin
for sh or ksh users
PATH=:/usr/ucb:/bin:/usr/bin export PATH
PATH=.:/usr/ucb:/bin:/usr/bin export PATH
把 "." 放在 PATH 中是很方便的,若要执行 "./a.out" 只要打 "a.out"
即可,但是这么做会有大麻烦。
当把 "." 放在 PATH 的最前面情况下。若你目前所在目录是如 "/tmp" 这
样大家都可以写的地方。如果有别的使用这放了一个名为 "ls" 的程式在
那里,而你打入 "ls" 那你可能就死得非常难看了。
把 "." 放到 PATH 的结尾是个比较好的做法:
setenv PATH /usr/ucb:/bin:/usr/bin:.
这么一来,当你在 "/tmp" 中打 "ls" 保 shell 就会先找 "/usr/ucb",
"/bin" 与 "/usr/bin" 里的 "ls"。减少了一点危险。不过仍然不是百分之
百安全。假如你是个笨拙的打字者,有一天你把 "ls -l" 打成 "sl -l",
而又有一个聪明的使用者能猜到这种常见的打字错误,在 "/tmp" 底下放
了一只 "sl",你还是得死。所以啊,千万要小心。
--
※ 来源:.饮水思源 bbs.sjtu.edu.cn.[FROM: 202.112.26.40]
--
※ 来源:·哈尔滨紫丁香站 bbs1.hit.edu.cn·[FROM: qlu@hitcrc.hitcrc.hi]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:414.350毫秒