10

How can I alias ... to ../.. in Bash?

I am aware that other answers allow alias '...'='cd ../..' but I'd like to be able to address the directory two levels up with other commands, allowing for:

cd ...
ls ...
realpath ...

I have tried alias '...'='../..' but when trying to use this I get the following:

$ alias '...'='../..'
$ type ...
... is aliased to '../..'
$ cd ...
bash: cd: ...: No such file or directory

I'm aware aliases are intended for use with commands. Is there a way to achieve the same functionality for a path in this case?

4
  • 3
    Similar, if not a dupe of Paths as bash aliases only work in parent directories?
    – Kusalananda
    Commented May 16, 2023 at 14:25
  • Short of fixing the OS's path resolving logic or intercepting all system calls that take pathnames to preprocess them, any solutions would likely have problems with one or more of a) using ... as a part of a path (cat .../somefile), b) using ... through a variable (f=.../foo; cat "$f"), and/or c) non-file arguments that contain ... (git commit -m "let's see if this works..."). Which isn't to say you shouldn't do that if you like the idea, just that there might be some gotchas that make it less generally useful than it could be.
    – ilkkachu
    Commented May 16, 2023 at 20:53
  • 1
    Not a serious answer, but ln -s ../.. ... :)
    – rrauenza
    Commented May 17, 2023 at 7:06
  • A not terribly good idea: redefining cd (stackoverflow.com/questions/28783509/…). Check if $1=="..." then builtin cd ../.. else cd $1. Commented May 17, 2023 at 9:47

4 Answers 4

27

If switching to zsh is an option, you could use global aliases there for that:

alias -g ...=../..

But in any case that's only expanded when ... is recognised as a full delimited token on its own. It will be expanded in echo ... or echo ...> file or (echo ...) but not in echo .../x or echo ~/... or echo "...").

Normal csh-style aliases, whether it's in bash or zsh are only expanded when in command position or after another alias expansion ending in a blank character. In zsh, though, if you set the autocd option, entering ../.. or another directory as the command name gets turned into cd ../.., so

$ set -o autocd
$ alias ...=../.. ....=../../..
$ ...

Would be a way to cd into ../.., but then again so would alias ...='cd ../..' or ...() cd ../...

A better approach that I've been using for many years is to bind the . key to something that inserts /.. instead of . if what's left of the cursor ends in ..:

magic-dot() {
  if [[ $LBUFFER = (|*[[:blank:]/]).. ]]; then
    repeat ${NUMERIC-1} LBUFFER+=/..
  else
    zle self-insert
  fi
}
zle -N magic-dot
bindkey . magic-dot
bindkey -M isearch . self-insert # restore . in incremental search
                                 # so it doesn't exit isearch there

Then entering ... gets immediately and visually transformed to ../.. and pressing . again changes it to ../../.. and so on. If in emacs mode, .. followed by Alt+4,. makes it ../../../../.. for instance (/.. appended 4 times) using the usual NUMERIC prefix handling.

It used to sometimes get in the way when copy/pasting things that contain ..., but that's no longer a problem now that bracketed paste support has been introduced. To enter a literal ..., you can press Ctrl+v before the third . or use backslashes, quoting or anything that makes so what's left of the cursor ($LBUFFER above) doesn't end in <blanks-or-slash>.. such as .\.., ..\., whatever/'...'...

An intentional limitation of the above is that for instance in sort -o..., the ... is not expanded. Use sort -o ... instead. Same goes for cd '.../***' -> cd ...'/***'. You can change the test above to [[ $LBUFFER = (|*[^.]).. ]] for the ... to be expanded to ../.. in more contexts although that would increase the probability of it being expanded in places you wouldn't like it to.

We can make the numeric argument handling a bit more useful by allowing cd Alt+4. to cd 4 levels up (expand to ../../../..) for instance with:

magic-dot() {
  if (( NUMERIC )) && [[ $LBUFFER = (|*[[:blank:]/:]) ]]; then
    LBUFFER+=..
    (( NUMERIC-- ))
  fi
    
  if [[ $LBUFFER = (|*[[:blank:]/]).. ]]; then
    repeat ${NUMERIC-1} LBUFFER+=/..
  else
    zle self-insert
  fi
}
zle -N magic-dot
bindkey . magic-dot
bindkey -M isearch . self-insert

As mentioned by @Gilles in comment, recent versions of bash have added limited support for editing the line buffer in commands bound to keys, so you could do something similar there with:

insert_before_cursor() {
  # for the equivalent of zsh's repeat $1 LBUFFER+=$2
  local i
  for (( i = 0; i < $1; i++ )); do
    READLINE_LINE=${READLINE_LINE:0:READLINE_POINT}$2${READLINE_LINE:READLINE_POINT}
    (( READLINE_POINT += ${#2} ))
  done
}

magic-dot() {
  (( ${READLINE_ARGUMENT-1} > 0 )) || return
  if [[ -v READLINE_ARGUMENT && ${READLINE_LINE:0:READLINE_POINT} = ?(*[[:blank:]/]) ]]; then
    insert_before_cursor 1 ..
    (( READLINE_ARGUMENT-- ))
  fi

  if [[ ${READLINE_LINE:0:READLINE_POINT} = ?(*[[:blank:]/]).. ]]; then
    insert_before_cursor "${READLINE_ARGUMENT-1}" /..
  else
    insert_before_cursor "${READLINE_ARGUMENT-1}" .
  fi
}
bind -x '".":magic-dot'

# work around a bug in current versions of bash for the numeric
# argument to work properly, that means however that you lose the
# insert-last-argument normally bound to Meta-. (also on Meta-_)
bind -x '"\e.":magic-dot'

You need bash 4.0 or above for $READLINE_LINE (not $READLINE_LINE_BUFFER as misspelled in some of the documentation), and $READLINE_POINT, and 5.2 or above for $READLINE_ARGUMENT, though as noted above it currently doesn't work properly yet.

4
  • 1
    alias -g ...=../.. is not really useful because it only works when ... is alone, it doesn't make .../foo work as an equivalent of ../../foo. Whereas magic-dot is genuinely useful. (But I think modern versions of bash can do it so it's no longer a reason to switch to zsh.) Commented May 17, 2023 at 7:09
  • 1
    I wonder if it is at least theoretically possible to implement magic-dot for bash instead of zsh. It sounds cool, but I can't switch to zsh on my company PB because many tools depend on Bash.
    – Neinstein
    Commented May 19, 2023 at 10:09
  • @Neinstein, that reminds me of the 90s when it wasn't uncommon for people to get stuck with csh because that was what everyone else was using, and you had to go through hoops to be able to use a proper shell. Here it's not so bad, zsh has a ksh-emulation mode with which it should be able to run most bash code. In any case, using zsh as your interactive shell doesn't prevent you from running bash scripts or scripts in any language. Anyway, see edit for a bash version. Commented May 19, 2023 at 15:26
  • 1
    Neat, I'll try it! re bash - in my case it's not really the scripts but the whole environment. I'm a simulational physicist, and half of the codes are very obscure, were written by some random genius in like the 70s, half in Bash half in Fortran, documented in French or German, and used by about 20 people around the whole world. I'm happy if I can pray them to compile and run even in Bash, let alone challenging the gods of doom by using an alternative shell.
    – Neinstein
    Commented May 22, 2023 at 21:50
13

You can't use aliases for this, aliases are about things you execute, not things you pass to a command as arguments. You could use a variable, but that would mean typing something slightly different. But if you use a short variable name, it can be just two characters:

$ pwd
/home/terdon/foo/bar
$ v="../.."
$ realpath "$v"
/home/terdon

If that works for you, you can add the variable definition to your ~/.profile or ~/.bash_profile files and you will then have access to it in your shell sessions:

v="../.."
0
5

There are multiple ways you can reach your goal

METHOD 1: Modify the source code for bash

This method is version specific but is still recommended as its the good way to get things done. What I did is modified builtins/cd.def such that before it checks for absolute path it checks if the first argument is ... if so it sets the dirname to ../.. (sounds like alias only)

STEP 1: Download bash-5.1.16.tar

wget http://ftp.gnu.org/gnu/bash/bash-5.1.16.tar.gz

STEP 2: Fetch & apply the patch bash_5.16_cd_three_dot.patch

tar -xf bash-5.1.16.tar.gz
cd bash-5.1.16
wget https://gist.githubusercontent.com/s0ubhik/6685effe6e46f6a840bb480c5d657933/raw/5736ce64cbb0b009d872c21f40d69c8ee2942b74/bash_5.16_cd_three_dot.patch
patch -p0 < bash_5.16_cd_three_dot.patch

STEP 3: Compile and run

./configure
make
./bash

cd ... bash

METHOD 2: Enable autocd in shopt

This methods does not do exactly what you want but i think it is better rather using clever alias techniques

shopt -s autocd

Now if you want to go one directory back just use .. and for two ../.. autocd cd

METHOD 3: Use clever alias

This method works but is not recommend as might be problematic to various scripts the use cd. you may add this to your .bashrc or create a seprate file and then include the file in .bashrc

cd_mod(){
  if [ "$1" == "..." ]; then
    \cd ../../
  else
    \cd $@
  fi
}

alias cd=cd_mod

cd_mod cd

3
  • 4
    Modifying the source code for Bash is probably not recommended, unless you convince the Bash upstream to merge the change.
    – Kaz
    Commented May 18, 2023 at 23:05
  • 1
    I don't see how an alias would be problematic, but modifying the source code wouldn't be.
    – wizzwizz4
    Commented May 19, 2023 at 12:52
  • 2
    question says " I'd like to be able to address the directory two levels up with other commands", which none of your suggestions accomplishes. Now, patching the Bash source code could indeed accomplish the goal, but not by changing the cd builtin. It would have to be done in global argument processing, such as where ~ gets expanded to the home directory.
    – Ben Voigt
    Commented May 19, 2023 at 14:22
3

You can't use alias directly because ... is an argument to cd, but I have seen some people alias cd.. to be cd .., so you could do similar with all of the commands:

alias cd...='cd ../..'

On the other hand...

$ cat ~/mycd 
if [ "$1" = '...' ]
then
  # Use \cd to prevent recursion
  \cd ../..
else
  \cd "$1"
fi

And then

alias cd=". ~/mycd"

Alternatively,

mycd() { if [ "$1" = '...' ]; then \cd ../..; else \cd "$1"; fi; }
alias cd="mycd"
5
  • 2
    You could also do alias cd='cd ' ...=../.. as aliases that end in a space make the next word get checked for alias expansion too. But that has the same problem, you have to do it individually for each command, and it only works to expand ... in the position right after the command word, and only if it's ... exactly, so cp foo.txt .../bar won't work
    – ilkkachu
    Commented May 17, 2023 at 5:56
  • @Kusalananda I was thinking of not quoting $1 in the \cd $1 case ... but it does look like cd "" is the same as cd with no args, so I guess it is fine!
    – rrauenza
    Commented May 17, 2023 at 7:02
  • 4
    @rrauenza You could also use cd "$@" to pass exactly the arguments that were passed to the function. Note too that you can avoid that extra alias by naming the function cd.
    – Kusalananda
    Commented May 17, 2023 at 8:49
  • Probably worth mentioning how you change into a directory that's actually called ... (you'll need to write it as ./...). Commented May 18, 2023 at 14:36
  • question says " I'd like to be able to address the directory two levels up with other commands", which none of your suggestions accomplishes.
    – Ben Voigt
    Commented May 19, 2023 at 14:22

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .