I have a function in my .bashrc file and I can't wrap my head around it. I know what it does, it steps up X many directories with cd

Here it is:

up()
{
    local d=""
    limit=$1
    for ((i=1 ; i <= limit ; i++))
      do
        d=$d/..
      done
    d=$(echo $d | sed 's/^\///')
    if [ -z "$d" ]; then
      d=..
    fi
    cd $d
}

But can you explain these three things from it for me?

  1. d=$d/..
  2. sed 's/^\///'
  3. d=..

Why not just do like this:

up()
{
    limit=$1

    for ((i=1 ; i <= limit ; i++))
    do
        cd ..
    done
}

Usage:

<<<>>>~$ up 3
<<<>>>/$
up vote 24 down vote accepted
  1. d=$d/.. adds /.. to the current contents of the d variable. d starts off empty, then the first iteration makes it /.., the second /../.. etc.

  2. sed 's/^\///' drops the first /, so /../.. becomes ../.. (this can be done using a parameter expansion, d=${d#/}).

  3. d=.. only makes sense in the context of its condition:

    if [ -z "$d" ]; then
      d=..
    fi
    

    This ensures that, if d is empty at this point, you go to the parent directory. (up with no argument is equivalent to cd ...)

This approach is better than iterative cd .. because it preserves cd - — the ability to return to the previous directory (from the user’s perspective) in one step.

The function can be simplified:

up() {
  local d=..
  for ((i = 1; i < ${1:-1}; i++)); do d=$d/..; done
  cd $d
}

This assumes we want to move up at least one level, and adds n - 1 levels, so we don’t need to remove the leading / or check for an empty $d.

Using Athena jot (the athena-jot package in Debian):

up() { cd $(jot -b .. -s / "${1:-1}"); }

(based on a variant suggested by glenn jackman).

  • Yep, $OLDPWD getting trampled on came to mind. And on zsh with cd set to use the dirstack, that too. – muru Oct 18 at 10:08
  • @StephenKitt Thanks a lot for the information, it is great to have this resource in my learning. – somethingSomething Oct 18 at 10:11
  • or even up() { local limit=${1:-1}; cd $(jot -b .. -s / "$limit"); } – glenn jackman Oct 18 at 12:50
  • @glenn I didn’t know that one — is that the Athena jot? – Stephen Kitt Oct 18 at 15:15
  • 1
    @kundor it ensures the default is 0 (see shell arithmetic). We could use ${1:-1} instead, as in glenn’s variant. – Stephen Kitt Oct 18 at 16:37

But can you explain these three things from it for me?

  1. d=$d/..

    This concatenates the present contents of var d with /.. and assign it back to d.
    The end result is to make d a repeated string like /../../../...

  2. sed 's/^///'

    Remove the leading / from the string supplied, d (echo $d) in the code you posted.
    Probably better written as sed 's|^/||' to avoid the backslash.

    An alternative (faster and simpler) is to write d=${d#/}.

  3. d=..

    Assign the string .. to the var d.
    This only makes sense as a way to ensure that d has at least one .. in case the test if [ -z "$d" ]; then signals that the var d is empty. And that can only happen because the sed command is removing one character from d.
    If there is no need to remove the character from d, there is no need for sed or if.


The code in your question will always move up at least one dir.

Better

  • local d is enough to make sure that the variable is empty, nothing else is needed.
    However, local works only in some shells like bash or dash. In specific, ksh (as yash) doesn't have a local command. A more portable solution is:

    [ "$KSH_VERSION$YASH_VERSION" ] && typeset c='' d='' i='' || local c='' d='' i=''
    
  • The for(( syntax is not portable. Better use something like:

    while [ "$((i+=1))" -lt "$limit" ]; do
    
  • Robust.
    If the values given to the function first argument could be negative or text, the function should be robust to process those values.
    First, lets limit the values to only numbers (I'll use c for count):

    c=${1%%[!0-9]*}
    

    And then limit the count value only to positive values:

    let "c = (c>0)?c:0"
    

This function will hapily accept 0 or any text (without errors):

up()
{
    [ "$KSH_VERSION$YASH_VERSION" ] && typeset c='' d='' i='' || local c='' d='' i=''
    c=${1%%[!0-9]*}
    c=$(((c>0)?c:0))
    while [ "$((i+=1))" -le "$c" ]; do d="$d../"; done
    echo \
        cd "${d%/}"         # Removing the trailing / is not really needed for cd
                            # but it is nice to know it could be done cleanly.
}

up 2
up 0
up asdf

Remove the echo \ when you have tested the function.

Your Answer

 

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.