A long time ago I created a script for moving up directories very quickly in the command line using the command up. You can find usage notes here.

It's a very simple script with just 8 lines of source code, as follows:

if [ -z "$1" ]; then
    cd ..
else
    for i in `seq 1 $1`;
    do
        cd ..
    done
fi

I've never personally had any problems with it since I made it and started using it myself — but are there any accidental or malicious inputs (particularly with the blind injection of $1) that might cause this to do something bad that I'm not aware of?

Given that the up command isn't used for anything in any command line I'm aware of, I'd like to promote more widespread usage of this script file so that people can type up instead of cd .. all the time, saving three keystrokes (or more if they want to move up more directories) for a very common operation.

  • zsh: ... Also, ..., ...., ....., ... – naught101 Oct 26 '15 at 1:36
  • 1
    @naught101: that's why I tagged this question bash. :P – Joe Z. Oct 26 '15 at 1:47
  • 1
    Nice approach, but I simply used alias f='cd ..;';alias ff='cd ..;cd ..;'alias fff='cd ..;cd ..;cd ..;';alias ffff='cd ..;cd ..;cd ..;cd ..;'; alias fffff='cd ..;cd ..;cd ..';cd ..;cd ..; in bashrc because there's only so many times you'd want to go up and the "f" key is easily accessible. Just a matter of typing "f" a few times and then hit enter instead of type up 5. The "f" stands for "fall" because from childhood, I considered cd dirName to be "climbing" directories and cd .. to be falling from a climbed directory. – Nav Oct 26 '15 at 9:18
  • 3
    @Nav: if you were doing that, why not use cd ../.. for ff? You iterated them independently by hand. – Joe Z. Oct 26 '15 at 14:56
  • I've been satisfied with just cd to take me home. Since all the files I own and work on are within there. And usually getting back to working files are less than four or five tab-competes deep. Though more commonly cd ~/where/ever/. [Just speaking generally about my personal cd usage.] – ThorSummoner Oct 26 '15 at 19:18
up vote 15 down vote accepted

Your recommendation is to define alias up=". path/to/up" so that when you type up 3, it expands to . up 3. However, since you want to take an optional argument and affect the state of the current shell, I think you would be better off defining a shell function instead.

As it turns out, the [ -z "$1" ] special case is not necessary, since seq 1 just expands to 1.

You end up executing n separate cd .. commands for up n. This leads to a usability bug: cd - or cd $OLDPWD, which normally take you back to the previous directory, don't work the way I expect.

Suggested solution:

up() {
    cd $(for i in $(seq 1 $1) ; do echo -n ../ ; done)
}
  • So defining up in that way makes a literal cd - useless? Or were you expecting up - to work as cd - as well? – Joe Z. Oct 26 '15 at 1:06
  • 5
    @JoeZ. the former - cd - will return you back one level after calling up n with n > 1. This causes an issue if you want to get back to where you were before you called up n. Concating the command into a single cd will preserve the functionality. – Boris the Spider Oct 26 '15 at 16:18
  • This is an extremely elegant solution and should be the accepted answer. Kudos. – Wildcard Nov 29 '15 at 8:27

This is a decent concept, but by looping on the cd you loose some of the value of the $OLDPWD function in the shell. For example, I often use the special construct cd - in a shell, and that changes directory to the one you were in before.

Your code will make that impossible.

I would instead recommend that you instead build up a chain of ../ string values, like ../../../../ for 4 directories, and then just call cd once, which will preserve the cd - function, and the $OLDPWD.

Additionally, this would be a good feature to include as a function in your code, rather than a script. Bash shell likes functions, and they make life easier.

Finally, if someone supplies a non-number as an argument, it will do odd things.

I played with your code, and came up with:

up () {
    local count=$1
    if [ -z "$count" ]; then
        cd ..
        return
    fi

    test "$count" -eq "$count" || return 1

    local todir=""
    for i in `seq 1 $count`;
    do
      todir="../$todir"
    done
    cd $todir
}

The features of the above code I like are:

  • it is a function of the shell, so there's no additional script called.
  • it checks the value is a number, by doing a numeric comparison on the value: test "$count" -eq "$count" (That will throw an error if the inputs are not integers)
  • it only does a single 'cd', so things like cd - still work.

I would add that to my ~/.bashrc file, or source it in to my current shell.

200_success's answer uses the evil eval which in his context was unnecessary.

Update: Oh, what, ? Let's use some brace expansion + evil things:

# This time we have to be evil. All code in this answer is CC0.
up() { [ "$1" -eq "$1" ] &>/dev/null || set -- 1; "cd \$(printf '../%.s' {1..$1})"; }

local is not a that nice solution since POSIX doesn't contain local. After some searching, I found out that it's possible to use printf to duplicate strings. After replacing the brace expansion with seq, here is what I got:

up() { cd "$(printf '../%.s' $(seq 1 $1))"; }

P.S.: The use of seq for simple looping is sometimes considered harmful, especially when used with for var in. To make you feel less guilty:

# integer seq, @copyright CC0.
iseq() (
    : ${iseq_fmt='%s\n'}
    case "$#" in
        (1)  i=1  last=$1 incr=1;;
        (2)  i=$1 last=$2 incr=1;;
        (3)  i=$1 last=$3 incr=$2;;
        (*)  return 2;;
    esac
    [ "$i" -eq "$i" ] && [ "$incr" -eq "$incr" ] && [ "$last" -eq "$last" ] || return 2
    while [ "$i" -le "$last" ]; do
        printf "$iseq_fmt" "$i"
        : $((i = i + incr))
    done
)
  • In my solution, i is scoped to a subshell. The prior value of $i has no influence on the execution, nor does the execution alter the value of $i in the main shell. – 200_success Oct 26 '15 at 0:38
  • @200_success Oh right, command substitution. – Arthur2e5 Oct 26 '15 at 0:46
  • 1
    @200_success But you still don't need that eval actually. – Arthur2e5 Oct 26 '15 at 0:47

You probably want to forward any options passed to cd, particularly -L, -P, -e and -@. Luckily, none of those take any arguments, so it's straightforward to catch them:

up() {
    local options=()
    for i in "$@"
    do
        case "$i" in
          -*) options+=("$i") ;;
          [1-9]*)
             test "$i" -eq "$i" &&
             cd "${options[@]}" $(perl -e "print'../'x$i;")
             return ;;
        esac
    done

    # If we got here, there were no non-option arguments
    cd "${options[@]}" ..
}

I might name it .. rather than up.

  • Do you want [1-9]* or ([1-9][0-9]*)?? If you typed up 10 what would happen? – Joe Z. Nov 8 at 23:35
  • I'm confusing them with regexes. Thanks for telling me. – Joe Z. 2 days ago
  • Er, yes, I meant regexps - sorry for the confusion. Anyway, 10 matches (I tested it). – Toby Speight yesterday
  • What happens if you type up 1P? – Joe Z. 22 hours ago
  • Then the test "$i" -eq "$i" fails and nothing happens (it would be nicer if it gave the user an error message; that's an exercise for the reader) – Toby Speight 22 hours ago

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.