对于作业,我应该创建一个名为my_which.sh
的脚本,它将执行与Unix命令相同的操作,但是使用for循环覆盖if。&。 #34;我也不被允许在我的剧本中打电话。
我对此很陌生,并且一直在阅读教程,但我对如何开始感到困惑。不是which
只列出命令的路径名吗?
如果是这样,我怎样才能显示正确的路径名而不调用哪个,并且使用for循环和if语句?
例如,如果我运行我的脚本,它将echo %
并等待输入。但是,我如何将其翻译为查找目录?那么它会是这样的吗?
#!/bin/bash
path=(`echo $PATH`)
echo -n "% "
read ans
for i in $path
do
if [ -d $i ]; then
echo $i
fi
done
我很感激任何帮助,甚至任何可以帮助我开始这方面的入门教程。我真的很困惑我应该如何实现这一点。
答案 0 :(得分:2)
安全地拆分PATH
变量 。这是在分隔符处拆分字符串的一般方法,对于任何可能的字符(包括换行符)都是100%安全的:
IFS=: read -r -d '' -a paths < <(printf '%s:\0' "$PATH")
我们人为地添加了:
,因为如果PATH
以尾随:
结尾,则可以理解当前目录应该在PATH
中。虽然这很危险而且不推荐,但如果我们想模仿which
,我们也必须考虑到这一点。如果没有此尾随冒号,PATH
之类的/bin:/usr/bin:
将被分割为
declare -a paths='( [0]="/bin" [1]="/usr/bin" )'
而对于这个尾部冒号,结果数组是:
declare -a paths='( [0]="/bin" [1]="/usr/bin" [2]="" )'
这是其他答案错过的一个细节。当然,只有在PATH
设置且非空时才会这样做。
使用此分割PATH
,我们将使用for循环检查是否可以在给定目录中找到该参数。请注意,仅当参数不包含/
字符时才应执行此操作!这也是其他答案遗漏的原因。
我的which
版本处理一个唯一选项-a
,打印每个参数的所有匹配路径名。否则,仅打印第一个匹配项。我们也必须考虑到这一点。
我的版本处理以下退出状态:
0 if all specified commands are found and executable 1 if one or more specified commands is nonexistent or not executable 2 if an invalid option is specified
我们也会处理。
我想以下情况相当忠实地模仿了我which
的行为(而且它是纯粹的Bash):
#!/bin/bash
show_usage() {
printf 'Usage: %s [-a] args\n' "$0"
}
illegal_option() {
printf >&2 'Illegal option -%s\n' "$1"
show_usage
exit 2
}
check_arg() {
if [[ -f $1 && -x $1 ]]; then
printf '%s\n' "$1"
return 0
else
return 1
fi
}
# manage options
show_only_one=true
while (($#)); do
[[ $1 = -- ]] && { shift; break; }
[[ $1 = -?* ]] || break
opt=${1#-}
while [[ $opt ]]; do
case $opt in
(a*) show_only_one=false; opt=${opt#?} ;;
(*) illegal_option "${opt:0:1}" ;;
esac
done
shift
done
# If no arguments left or empty PATH, exit with return code 1
(($#)) || exit 1
[[ $PATH ]] || exit 1
# split path
IFS=: read -r -d '' -a paths < <(printf '%s:\0' "$PATH")
ret=0
# loop on arguments
for arg; do
# Check whether arg contains a slash
if [[ $arg = */* ]]; then
check_arg "$arg" || ret=1
else
this_ret=1
for p in "${paths[@]}"; do
if check_arg "${p:-.}/$arg"; then
this_ret=0
"$show_only_one" && break
fi
done
((this_ret==1)) && ret=1
fi
done
exit "$ret"
为了测试参数是否可执行,我正在检查它是否是可执行的常规文件 1 :
[[ -f $arg && -x $arg ]]
我猜这接近于which
的行为。
1 由于@ mklement0指出(谢谢!)-f
测试,当对符号链接应用时,测试符号链接的目标的类型。
答案 1 :(得分:1)
#!/bin/bash
#Get the user's first argument to this script
exe_name=$1
#Set the field separator to ":" (this is what the PATH variable
# uses as its delimiter), then read the contents of the PATH
# into the array variable "paths" -- at the same time splitting
# the PATH by ":"
IFS=':' read -a paths <<< $PATH
#Iterate over each of the paths in the "paths" array
for e in ${paths[*]}
do
#Check for the $exe_name in this path
find $e -name $exe_name -maxdepth 1
done
答案 2 :(得分:0)
这与接受的答案类似,区别在于它没有设置IFS并检查是否设置了执行位。
#!/bin/bash
for i in $(echo "$PATH" | tr ":" "\n")
do
find "$i" -name "$1" -perm +111 -maxdepth 1
done
将其另存为my_which.sh
(或其他名称)并将其作为./my_which java
等运行。
但是,如果有&#34; if&#34;需要:
#!/bin/bash
for i in $(echo "$PATH" | tr ":" "\n")
do
# this is a one liner that works. However the user requires an if statment
# find "$i" -name "$1" -perm +111 -maxdepth 1
cmd=$i/$1
if [[ ( -f "$cmd" || -L "$cmd" ) && -x "$cmd" ]]
then
echo "$cmd"
break
fi
done
您可能需要查看此link来确定&#34; if&#34;中的测试。
答案 3 :(得分:0)
对于完整,坚如磐石的实施,请参阅gniourf_gniourf's answer。
这是一个更简洁的替代方案,可以使用find
[每个名称进行调查]的 单调用。
OP稍后澄清说,应该在循环中使用if
语句,但问题是否足以保证考虑其他方法。
天真的实现甚至可以作为一个单行,如果你愿意做一些假设(该示例使用'ls'作为可执行文件来定位):
find -L ${PATH//:/ } -maxdepth 1 -type f -perm -u=x -name 'ls' 2>/dev/null
这些假设 - 在很多但不是所有情况下都会存在 - 是:
$PATH
不得包含在shell扩展中使用不带引号的结果时的条目(例如,没有导致分词的嵌入空格,没有会导致路径名扩展的*
等字符)< / LI>
$PATH
不得包含空条目(必须将其解释为当前目录)。说明:
-L
告诉find
调查符号链接的目标而不是符号链接本身 - 这可以确保-type f
<也可以识别可执行文件的符号链接/ LI>
${PATH//:/ }
替换所有:
个字符。在$PATH
中,每个都有一个空格,导致结果 - 由于不加引号 - 作为由空格分割的单个参数传递。-maxdepth 1
指示find
仅直接查看每个指定的目录,而不是在子目录中-type f
仅匹配文件,而不匹配目录。-perm -u=x
仅匹配当前用户(u
)可以执行的文件和目录(x
)。2>/dev/null
会抑制可能源于$PATH
中不存在的目录或由于缺少权限而无法尝试访问文件的错误消息。这是一个更强大的脚本版本:
注意:
\n
字符的情况 - 但是,这在实践中极为罕见,可能会导致整体问题更严重。#!//bin/bash
# Assign argument to variable; error out, if none given.
name=${1:?Please specify an executable filename.}
# Robustly read individual $PATH entries into a bash array, splitting by ':'
# - The additional trailing ':' ensures that a trailing ':' in $PATH is
# properly recognized as an empty entry - see gniourf_gniourf's answer.
IFS=: read -r -a paths <<<"${PATH}:"
# Replace empty entries with '.' for use with `find`.
# (Empty entries imply '.' - this is legacy behavior mandated by POSIX).
for (( i = 0; i < "${#paths[@]}"; i++ )); do
[[ "${paths[i]}" == '' ]] && paths[i]='.'
done
# Invoke `find` with *all* directories and capture the 1st match, if any, in a variable.
# Simply remove `| head -n 1` to print *all* matches.
match=$(find -L "${paths[@]}" -maxdepth 1 -type f -perm -u=x -name "$name" 2>/dev/null |
head -n 1)
# Print result, if found, and exit with appropriate exit code.
if [[ -n $match ]]; then
printf '%s\n' "$match"
exit 0
else
exit 1
fi