How do I change to a near-identical path with a different low-level parent? If you’re working in for instance ~/foobar/foo/data/images/2020/01/14/0001/
and need to get to the same path in bar
instead of foo
, how can you get there without typing out cd ~/foobar/bar/data/images/2020/01/14/0001/
? Surely there’s some elegant and/or kludgy solution.
5 Answers
In some shells, e.g. ksh
and zsh
, doing cd word1 word2
would change to a directory given by changing the first occurrence of word1
in the pathname of the current directory to word2
.
For example, in the zsh
shell:
$ pwd
/usr/local/sbin
$ cd sbin bin
/usr/local/bin
$
In other shells that support the non-standard ${variable/pattern/replacement}
parameter substitution originally found in ksh93
, you may use ${PWD/word1/word2}
to create the pathname of the directory to change into:
$ pwd
/usr/local/sbin
$ cd "${PWD/sbin/bin}"
$ pwd
/usr/local/bin
In those shells (bash
, for example), you could even create your own naive cd
function to handle two arguments in the way that ksh
and zsh
does it, like so:
cd () {
if [ "$#" -eq 2 ] && [[ $1 != -* ]]; then
command cd -- "${PWD/$1/$2}" &&
printf 'New wd: %s\n' "$PWD"
else
command cd "$@"
fi
}
The [ "$#" -eq 2 ]
detects when the special cd
behavior should be triggered (when there are exactly two command line arguments), but we test with [[ $1 != -* ]]
to not trigger the special behavior if you use an option with cd
. Using command cd
instead of cd
inside the function avoids calling the function recursively.
Testing that in bash
:
$ cd /usr/local/sbin
$ cd sbin bin
New wd: /usr/local/bin
$ cd local ''
New wd: /usr/bin
$ cd bin sbin
New wd: /usr/sbin
$ cd sbin local/sbin
New wd: /usr/local/sbin
$ cd '*/' /
New wd: /sbin
Notice that the last command replaces using a pattern matching up to and including the last /
; the pattern must be quoted. To disallow patterns and to always treat the first argument as a word, use command cd "${PWD/"$1"/$2}"
in the function (notice the quoting of $1
).
To additionally force the replacement to only affect a complete directory name, use command cd "${PWD/"/$1/"/"/$2/"}"
.
Artificially inserting /
before and after both arguments would avoid matching substrings of directory names, but would make it incompatible with the way this works in zsh
and ksh
and it would no longer allow you to make substitutions in the last part of the directory path as there is no /
at the end (you can only provide a certain level of hand-holding before the extra "help" starts to be a hindrance).
This would make cd foo bar
work with the example that is in the question, though. You would otherwise have to make sure not to match foo
in foobar
in some other way, for example with cd foo/ bar/
.
-
1
${var/pattern/replacement}
is also from the Korn shell but came much later (ksh93). See also$var:s/foo/bar/
in (t)csh or zsh (already in the first release of csh in 2BSD in the late 70s) Commented Dec 19, 2020 at 8:16
It's easy.
cd "$( echo "$PWD" | sed -e 's%/foo/%/bar/%' )"
sed
uses the character following the s
command as its delimiter.
-
4You may want to read Why is printf better than echo? and Why does my shell script choke on whitespace or other special characters? Commented Dec 19, 2020 at 8:19
On the cli, you can use a modifier like this example:
$ mkdir -p foobar/foo/data/images/2020/01/14/0001/
mkdir: created directory 'foobar'
mkdir: created directory 'foobar/foo'
mkdir: created directory 'foobar/foo/data'
mkdir: created directory 'foobar/foo/data/images'
mkdir: created directory 'foobar/foo/data/images/2020'
mkdir: created directory 'foobar/foo/data/images/2020/01'
mkdir: created directory 'foobar/foo/data/images/2020/01/14'
mkdir: created directory 'foobar/foo/data/images/2020/01/14/0001/'
$ ^bar/foo^bar/bar^
mkdir -p foobar/bar/data/images/2020/01/14/0001/
mkdir: created directory 'foobar/bar'
mkdir: created directory 'foobar/bar/data'
mkdir: created directory 'foobar/bar/data/images'
mkdir: created directory 'foobar/bar/data/images/2020'
mkdir: created directory 'foobar/bar/data/images/2020/01'
mkdir: created directory 'foobar/bar/data/images/2020/01/14'
mkdir: created directory 'foobar/bar/data/images/2020/01/14/0001/'
Explanation:
The first command creates nested directories starting with foobar. The second command uses the modifier ^ to replace a string in the previous command with a new string. It is easy to make a mistake by typing the following:
^foo^bar^
However, this will change the command to
mkdir -p barbar/foo/data/images/2020/01/14/0001/
because it will also affect the leading foo. To avoid this, you can use some characters before or after the intended string.
I used mkdir in this example, but the cd is similar. HTH.
When I've needed to work between two directories quickly and more than a couple of times the following example shows what works for me:
$ pwd
/home/chris/tmp
$ ln -s /home/mythtv/video/tv/tmp
$ cd tmp
$ ls
$ cd ../
$ mv 852f.mkv tmp
$ ls tmp
852f.mkv
$ rm -i tmp
rm: remove symbolic link 'tmp'? y
$ ls /home/mythtv/video/tv/tmp
852f.mkv
Sometimes it's easier to use the mouse, assuming you're using a good terminal emulator on a graphical desktop. Or in screen
or a modern equivalent, you can use keyboard controls to copy/paste some text.
$ pwd
/some/long/path
copy/paste that path onto a cd
command (i.e. double-click on it to select the whole thing, or triple to get the whole line if there are spaces. If so then type a single-quote before pasting it).
Then use your shell's line-editing keys to edit an early component of it, e.g. in Bash's emacs
mode,
- ctrl-a to move the cursor to the start of the line
- ctrl-right-arrow (or alt-f) a couple times to go forward by words until the right component.
- alt-d or alt-backspace to delete a whole word forward or backward.
- type a replacement (with tab completion) and hit return
If you fumble something, it's only a cd
command so it won't do anything destructive.
set -o vi
you can use vi key bindings. When the cursor is at the end, you could do<ESC>F/;;;;lcwbar
(i.e. search for "/" backwards, repeat the search several times (";") until the cursor is on the "/" before "foo", move one character to the right (l
) and thenc
hangew
ord to "bar". I find this approach quite flexible.