How can I make my own "shell commands" (e.g. mkdir/cd combo)?

I can't tell how many times I have wished for a command that would both create a directory and move to that directory. Basically, I would like the equivalent of the following:

mkdir -p /arbitrarily/long/path; cd /arbitrarily/long/path

but only having to type the /arbitrarily/long/path once, something like:

mk-cd /arbitrarily/long/path

I tried creating a script to do this, but it only changes the directory within the script. I'd like the directory in the shell to have changed as well.

#!/bin/bash
mkdir $1
cd $1
export PWD=$PWD

How might I be able to make this work?


Solution 1:

The simplest way is to use a shell function:

mkcd() {
    mkdir -p -- "$1" && cd -- "$1"
}

Place it in your .bashrc file to make it be available to you just like another shell command.

The reason why it doesn't work as an external script is cd changes the current directory of the running script but doesn't affect the calling one. This is by design! Each process has its own working directory which is inherited by its children, but the opposite is not possible.

Unless part of a pipeline, run in the background or explicitly in a subshell, a shell function doesn't run in a separate process but in the same one, just like if the command has been sourced. The current directory shell can then be changed by a function.

The && used here to separate both commands used means, if the first command succeeds (mkdir), run the second one (cd). Consequently, if mkdir fails to create the requested directory, there is no point trying to go into it. An error message is printed by mkdir and that's it.

The -p option used with mkdir is there to tell this utility to create any missing directory that is part of the full path of the directory name passed as argument. One side effect is that if you ask to create an already existing directory, the mkcd function won't fail and you'll end up in that directory. That might be considered an issue or a feature. In the former case, the function can be modified for example that way which simply warns the user:

mkcd() {
    if [ -d "$1" ]; then
        printf "mkcd: warning, \"%s\" already exists\n" "$1"
    else
        mkdir -p "$1" 
    fi && cd "$1"
}

Without the -p option, the behavior of the initial function would have been very different.

If the directory containing the directory to create doesn't already exists, mkdir fails an so does the function.

If the directory to be create already exists, mkdir fails too and cd isn't called.

Finally, note that setting/exporting PWD is pointless, as the shell already does it internally.

Edit: I added the -- option to both commands for the function to allow a directory name starting with a dash.

Solution 2:

I would like to add an alternative solution.

Your script will work if you invoke it with the . or source command:

. mk-cd /arbitrarily/long/path

If you do this the export in the script is unnecessary. You can bypass typing the . by using an alias:

alias mk-cd='. mk-cd'

The reason this works is that a script normally runs in a sub-shell so its environment is lost on completion, but the . (or source) command forces it to run in the current shell.

This technique can be useful for command sequences which are too complex for a function or which are under development and are frequently edited: once the alias has been entered into .bash_aliases, you can edit the script at will without reinitialising.

Note the following:-

If you write a script which must be called with the ./source command, there are two ways to make sure of this:-

  1. For some reason, a script invoked with ./source need not be executable, so simply remove this permission and the script either will not be found in "$PATH" or will give a permission error if invoked with an explicit path.

  2. Alternatively, the following trick can be used at the head of the script:

bind |& read && { echo 'Must be run from "." or "source" command'; exit 1; }

This works because in a sub-shell bind issues a warning, though no error status: hence read is used to give an error on no input.

Solution 3:

Not a single command, but you don't have to retype all the path just use !$ (which is the last argument from the previous command in the shell history).

Example:

mkdir -p /arbitrarily/long/path
cd !$

Solution 4:

By creating aliases. Very popular way to create your own commands.

You can create an alias on the fly:

alias myalias='mkdir -p /arbitrarily/long/path; cd /arbitrarily/long/path'

That's it. If you want to keep that alias permanently (not just the current session), then put that command in a dotfile (typically ~/.bash_aliases or ~/.bash_profile).