将命令别名设置为切换到最新目录

3

我想在我的bashrc文件中添加一个别名,以便切换到当前目录中最新的子目录:

alias cdl="cd $(ls -t | head -n 1)"

问题是,当我执行源文件时,该命令仅被评估一次。如果我在更改目录后使用新的cdl命令,则它仍然会尝试更改为旧目录,该目录可能不存在于我的新位置,并且对我来说并不是很有用。
如何将此命令别名为每次运行都进行评估?

1
使用单引号:alias cdl='cd $(ls -t | head -n 1)'。尽管你的命令有误。 - gniourf_gniourf
2个回答

4
这里有一个安全的方法来执行你想要的操作:安全意味着它会找到最近的目录(不仅仅是文件,就像你的情况一样,虽然这可以修复),并且如果目录名称包含空格、通配符和换行符,它也能正常工作(通过添加适当的双引号,你的命令可以修复以处理空格和通配符,但不能处理换行符——有些人会认为处理换行符是不必要的;但既然可以处理,为什么不拥有一个强大的命令呢?)。
我们将使用函数而不是别名!
cdl() {
    local mrd
    IFS= read -r -d '' mrd < <(
        shopt -s nullglob
        mrd=
        for i in */; do
            if [[ -z $mrd ]] || [[ $i -nt $mrd ]]; then
                mrd=$i
            fi
        done
        [[ $mrd ]] && printf '%s\0' "$mrd"
    ) && cd -- "$mrd"
}

步骤说明

首先,我们要集中精力处理内部部分:

shopt -s nullglob
mrd=
for i in */; do
    if [[ -z $mrd ]] || [[ $i -nt $mrd ]]; then
        mrd=$i
    fi
done
[[ $mrd ]] && printf '%s\0' "$mrd"
nullglob shell选项可以使通配符在没有匹配项的情况下扩展为空。循环中使用了带有尾部斜杠的通配符for i in */,因此它会扩展到当前目录中的所有目录(或者如果没有目录,那么就是空的,感谢nullglob)。
我们将mrd变量初始化为空字符串,并循环遍历所有目录,循环变量为i:如果mrd为空或i扩展到比mrd更新的目录,则用i替换mrd
在循环之后,如果mrd仍然为空(如果根本没有找到目录),我们不做任何事情;否则,我们打印该目录名称并以尾随的空字节结尾。
现在,是外部部分:
IFS= read -r -d '' mrd < <( ... )

这将内部部分的输出(因此,要么没有内容,要么是最近目录的内容,并带有尾随的nullbyte)存储在变量mrd中。如果没有读取到任何内容,则read失败,否则read成功。在成功的情况下,我们高兴地cd进入最新的目录。


我想提出两点:

  • It's possible to write cdl as:

    cdl() {
        local mrd i
        for i in */; do
            if [[ -z $mrd ]] || [[ $i -nt $mrd ]]; then
                mrd=$i
            fi
        done
        [[ $mrd ]] && cd -- "$mrd"
    }
    

    As you can see, this doesn't set nullglob, which is mandatory here. But you don't want to set it globally. So you need to save old nullglob:

    local old_nullglob=$(shopt -p nullglob)
    

    and reset it at the end of your function:

    eval "$old_nullglob"
    

    While this is perfectly fine, I now try to avoid this, since if your function exits before completing (e.g., if user breaks its execution), nullglob wouldn't be reset. That's why I chose to run the loop in a subshell!

  • At this point, you might think that the following would solve the problem:

    local mrd=$( ... loop that outputs most recent dir... ) && cd -- "$mrd"
    

    Unfortunately, $(...) trims trailing newlines. So it's not 100% working, hence it's broken.

原来,在我看来最可靠的方法是使用
IFS= read -r -d '' v < <( .... printf '...\0' )

疯狂

如果你想要一个疯狂的函数:你可能已经注意到我给出的 cdl 函数不能处理隐藏目录。那么我们如何允许以下调用:

cdl .

您需要查找隐藏目录的功能吗?那么,我们是否允许向函数传递参数,这样就可以像下面这样调用:

cdl . /path/to/dir1 /path/to/dir2 ...

你希望将/path/to/dir1/path/to/dir2等目录的最近子目录cd进去吗?如果需要包括隐藏目录,则应该在第一个参数中设置开关。

cdl /path/to/dir1 .

使用cd命令可以进入/path/to/dir1目录下最新的非隐藏子目录以及当前目录,但是

cdl . /path/to/dir1 .

还包括隐藏目录。

cdl() {
    local mrd
    IFS= read -r -d '' mrd < <(
        [[ $1 = . ]] && shopt -s dotglob && shift
        (($#)) || set .
        shopt -s nullglob
        mrd=
        for d in "$@"; do
            if [[ ! -d $d ]]; then
                printf >&2 '%s is not a directory. Skipping.\n' "$d"
                continue
            fi
            [[ $d = / ]] && d=
            for i in "$d"/*/; do
                if [[ -z $mrd ]] || [[ $i -nt $mrd ]]; then
                    mrd=$i
                fi
            done
         done
        [[ $mrd ]] && printf '%s\0' "$mrd"
    ) && cd -- "$mrd"
}

我的下一个编辑将包括一次更新,它将允许调用cdl函数,在计算π的最后一位数时同时煮咖啡。


3
肯定会起作用,但是看到一个半行命名被转换成这个肯定会让 OP 感到晕眩 :) - anubhava
1
@anubhava 哈哈,这很多。我不确定这里发生了什么,但它像魔法一样运行良好。 - Mike
@Mike 或许我的修改会帮助你弄清正在发生什么? - gniourf_gniourf
1
@gniourf_gniourf 非常感谢您。如果可以的话,我会再次给您点赞! - Mike

2
如果您改用单引号,它就可以正常工作:
alias cdl='cd -- "$(ls -t | head -n 1)"'

请注意,命令中的ls可能不会提供最新的目录,它也可能提供一个文件,在这种情况下,该命令将不能像您期望的那样工作。添加--group-directories-first选项可能有助于解决这个问题。

谢谢,你是正确的。我把它改成了 cd $(ls -td */ | head -n 1),这样它只会列出目录。当然,如果没有这些目录,那就是另一个问题了。我想我应该为此添加一个检查。不过现在这个方法可以用了。 - Mike
没错,也许你最好现在写一个脚本而不是使用别名 :)。 - SukkoPera

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接