4

I have a common use case that I'd like to write a function for: I often want to cd to some directory relative to some file.

My current workflow looks like this:

$ gem which rspec/core | xargs echo -n | pbcopy
$ cd *paste and delete end until direcory looks right*

note: gem which rspec/core prints something like "/Users/joshcheek/.rvm/gems/ruby-1.9.3-p125/gems/rspec-core-2.10.0/lib/rspec/core.rb"

I'd like it to look like this:

$ gem which rspec/core | 2dir 3

Which will cd me into "/Users/joshcheek/.rvm/gems/ruby-1.9.3-p125/gems/rspec-core-2.10.0" (passing the argument "3" tells it to remove "lib/rspec/core.rb" from the end)

This is the best I've gotten so far:

2dir() {
  read dir
  for i in $(seq 1 $1)
    do
      dir="${dir%/*}"
  done
  cd "$dir"
}

But the cd changes the function's directory, not mine. I've tried swapping it with an alias, but can't figure out how to make anonymous functions or pass the argument.

Joshua Cheek
  • 30,436
  • 16
  • 74
  • 83

5 Answers5

9

I'd use:

2dir()
{
    name=${2:?'Usage: 2dir count path'}
    count=$1
    while [[ $count -gt 0 ]]; do name=$(dirname "$name"); ((count--)); done
    cd "$name"
}

and use it as:

2dir 3 $(gem which rspec/core)

This works where your pipeline can't. The cd in the pipe process affects that (sub-)shell, but cannot affect the current directory of the parent process. This function can be made to work.

And you can use your dir="${dir%/*}" in place of my dirname if you prefer, except that you'll end up in your home directory instead of the current directory (or root directory, depending on whether you gave a relative or absolute path name) if you specify 10 when there are only 5 components.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • Thank you. Good point with dirname. I also like the arg checking, I'll probably swap arg order so I can default the number to 2, since that's what I most often want. – Joshua Cheek May 16 '12 at 15:46
  • 1
    +1 with a minor quibble: `[[ > ]]` does alphabetic comparisons rather than numeric -- use `[[ -gt ]]` or `(( > ))` for numeric. Not that it matters in this particular case... – Gordon Davisson May 16 '12 at 18:51
  • Aargh! I keep forgetting that the shell has it backwards compared with Perl. Perl uses math-type operators for math-type comparisons; so much more sensible! – Jonathan Leffler May 16 '12 at 18:52
  • Use a `for` loop and it will maintain the variable for\* you: `for ((count = $1; count > 0; count--)); do name=$(dirname "$name"); done` (or `name=${name%/*}` or see [my answer](http://stackoverflow.com/a/10629589/26428)) \**no pun intended* – Dennis Williamson May 17 '12 at 03:51
2

Here's a variant of @Jonathan Leffler's suggestion to streamline usage a little -- it makes the count argument optional, and avoids the need for $( ) around the command:

2dir() {
# If first arg is a number, use it as a trim count; otherwise assume 2
if [[ "$1" =~ ^[0-9]+$ ]]; then
    count="$1"
    shift
else
    count=2
fi

if [[ $# -lt 1 ]]; then  # Make sure a command was specified
    echo "Usage: 2dir [count] command [commandargs ...]" >&2
    return 1
fi

name="$("$@")"  # Execute the remaining args as a command to get the target directory
while [[ $count -gt 0 ]]; do name=$(dirname "$name"); ((count--)); done
cd "$name"
}

Example uses:

2dir 3 gem which rspec/core
2dir gem which rspec/core
Gordon Davisson
  • 118,432
  • 16
  • 123
  • 151
1

The command gem which rspec/core | 2dir 3 is known as a "pipeline" in shell parlance. Each command in the pipeline is executed as a separate process. If one of the commands in the pipeline is a shell function, it may be executed by the current (interactive) shell process. But it is not guaranteed, and in your case this is not happening.

To fix your problem you just need to make sure that the function is evaluated in the interactive shell. You just need to fix the function and then use it differently. Here is the updated function:

2dir() {
  declare -ir snip="$1"
  declare dir="$2"
  for i in $(seq 1 "$snip"); do
      dir="${dir%/*}"
  done
  cd "$dir"
}

You use it like this:

$ 2dir 3 "$(gem which rspec/core)"
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
James Youngman
  • 3,623
  • 2
  • 19
  • 21
0

A shell script can't change the working directory of the interactive shell. Only an alias can do that, since it runs in the shell whose directory you are trying to change.

In other words:

There is a Linux process running the shell and accept commands from you. It has a working directory. When you tell it to execute a shell script, it creates a brand new process with an independent working directory, disconnected from the first.

bmargulies
  • 97,814
  • 39
  • 186
  • 310
  • I understand that. I'm not attached to the idea of a script, an alias will work just fine, I just want something I can pipe a path into, give it a number to specify how to modify the path, then have it cd me there. I'm happy to do that with an alias, I just can't figure out how. – Joshua Cheek May 16 '12 at 15:30
0

Based on Jonathan Leffler's answer, but without a loop:

2dir () {
    local levels name=${2:?"Usage: $FUNCNAME count path"};
    printf -v levels '%*s' "$1" '';
    cd "/${name%${levels// //*}}"
}

One annoyance is that it produces perfectly valid directories with leading double slashes (e.g. echo "$PWD" outputs "//foo/bar/baz" after using the function).

Another is that it's "too clever by half."

Edit:

Fixed the double slash issue:

2dir () {
    local levels name=${2:?"Usage: $FUNCNAME count path"};
    printf -v levels '%*s' $1 '';
    name=/${name%${levels// //*}};
    cd "${name/\/\///}"
}
Community
  • 1
  • 1
Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439