在bash脚本中创建一个命令

时间:2015-02-21 04:55:33

标签: bash shell

对于作业,我应该创建一个名为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

我很感激任何帮助,甚至任何可以帮助我开始这方面的入门教程。我真的很困惑我应该如何实现这一点。

4 个答案:

答案 0 :(得分:2)

  1. 安全地拆分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设置且非空时才会这样做。

  2. 使用此分割PATH,我们将使用for循环检查是否可以在给定目录中找到该参数。请注意,仅当参数不包含/字符时才应执行此操作!这也是其他答案遗漏的原因。

  3. 我的which版本处理一个唯一选项-a打印每个参数的所有匹配路径名。否则,仅打印第一个匹配项。我们也必须考虑到这一点。

  4. 我的版本处理以下退出状态:

       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
    

    我们也会处理。

  5. 我想以下情况相当忠实地模仿了我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