2
\$\begingroup\$

I find myself frequently needing to create a new directory and changing to it enough to want to combine the two commands, so I figured it was a good opportunity to practice scripting with bash.

The command I use to create and change to a directory is mkdir -p directory_name && cd directory_name, and that's the command that the mkkdir function wraps, essentially. The only additional stuff that I would say is really consequential is the check to make sure that the name of the directory does not begin with a hyphen.

Also, the script explicitly checks to make sure the directory does not already exist. This is not something that mkdir does, and when I'm using this command combination, I'm usually trying to specifically create a new directory. I've accidentally modified files that I did not mean to because I mkdir -p && cd'd into a directory that already existed.

# mkkdir version 1.4.0
#
# Create a new directory and move into it in the same
# command.
#
# If the specified directory already exists, the command
# will fail, as the assumption is that the directory is
# meant to be new.
#
function mkkdir() {
    # We begin by checking for the command-line argument
    # passed in by the user. It must be exactly one, since
    # we can only create and change into one directory, and
    # all of the valid program options result in short-
    # circuiting program execution.
    #
    if [[ $# -ne 1 ]]
    then
        # Since the number of command-line arguments was
        # not exactly one, notify the user of their error
        # and return with an error status code.
        echo 'Usage: mkkdir <DirectoryPath>' >&2 && return 1
    fi

    # Check whether the user requested the version number
    # of the program or the help menu. 
    case $1 in
    -h | --help)
        echo 'Usage: mkkdir <DirectoryPath>'
        echo 'Create new directory and cd to it'
        echo ''
        echo '  -h, --help     display this help and exit'
        echo '      --version  output version information and exit'
        echo 'This is free software: you are free to change and redistribute it.'
        echo 'There is NO WARRANTY, to the extent permitted by law.'
        return 0
        ;;

    --version)
        echo 'mkkdir version 1.4.0'
        return 0
        ;;

    # Check for invalid options. This function will thus
    # not be able to create directories that begin with a
    # hyphen, but that's so uncommon so as to be totally
    # fine.
    #
    # Usually an argument that begins with a hyphen
    # detected here will almost always be a mispelled
    # command-line option.
    -* | --*)
        echo "Invalid option detected: $1"
        return 1
        ;;
    esac

    # Passing in an empty string to the function will not
    # trigger any errors so far, since an argument was
    # technically passed in. Still, this edge case is
    # obviously not a valid use case, and we must therefore
    # validate the input string.
    if [ -z "$1" ]
    then
        # The input string is a zero-length string, so we
        # simply let the user know and exit with an error
        # status.
        echo 'The empty string is an invalid directory path.' >&2 && return 1
    fi
    
    # Check whether the specified directory exists. If it
    # does, return with an error status.
    if [[ -d "$1" ]]
    then
        # The directory exists so simply let the user know
        # and exit with an error status.
        echo "The specified directory already exists: $1" >&2 && return 1
    fi

    # Finally, create the directory and change the current
    # working directory to it using the cd command.
    mkdir -p "$1" && cd "$1"
}
```
\$\endgroup\$
4
\$\begingroup\$

Good

  • consistent indentation
  • good error checking
  • nice comments
  • great quoting of variable substitutions
  • yay for putting it all in a function
  • good use of case statement for processing options. (Typically this would be in a loop, but since you only allow for one argument above this it makes sense that there is no loop here.)

ShellCheck suggests

One of my favorite tools, Shellcheck.net, suggests:

  • Put in a shebang line. This is a good habit to have. It would let shellcheck know which shell-specific checks it could apply. And it indicates to users which shell you expect this code to be aimed for.
  • Your -* pattern is more general than your --* pattern, so you don't need both.
  • cd might fail so you should consider including a || something, which might print an error message or try something different. :) Since the cd is the last thing and it will print a reasonable error message I wouldn't worry about catching this error. Most of my scripts don't check for cd errors and the cd is never the last thing in the file. For critical code I have the set -e flag on for the whole script so any errors cause the script to exit without having to write error checking code for every file I/O operation.

My suggestions

  • All of your returns return a 1. This makes it impossible for automation built on top of this to determine which error occurred without parsing the output. Having a unique return code for each error type is a good habit to keep up so code that wasn't intended for automation is ready for automation.
  • I like that you've used [[ for conditionals for the most part, but you have one where you fell back on [. Using [[ is a best practice and I've never found a reason not to use it.
  • You have a number of echo && return lines. (A) The echo is unlikely to fail. (B) Even if the echo fails you probably still want to return. (C) It is clearer that you are returning if the return isn't buried at the end of the echo.
  • It would be easier to follow if the innards of your case...esac were indented another level.
  • I would tend to put the thens up next to the if with a ; next sort of thing.
  • It would be nice to mention in the docs that this file should be sourced and then the command will be available.

Conclusion

Overall, this is an excellent implementation for a challenge that has been around for decades. The code is very approachable and maintainable. The improvements suggested are mostly cosmetic. There are no apparent bugs. Keep up the great work.

\$\endgroup\$
2
  • \$\begingroup\$ Each and every one of these suggestions was something that I either completely missed or just plain had no idea about. Thanks! \$\endgroup\$ Oct 18 at 6:43
  • \$\begingroup\$ Many different error returns can be a maintenance nightmare. Look to the core utils for good examples; the only commands that have multiple failure codes tend to be the ones (such as timeout) that invoke other commands as their main function. \$\endgroup\$ Oct 18 at 7:37
4
\$\begingroup\$

The only thing here that's not standard shell is [[. That's easily replaced with [, which would give a more portable function: usable with any POSIX shell, not just Bash.

if [ -z "$1" ]

We could have included this test as part of the case.

mkdir -p "$1"

Always using -p might not be desirable. If I mis-type one of the directory components, I might be surprised to find a whole new directory created. Consider instead accepting more options, and passing them on to mkdir. That would save us having to do the -d test ourselves, and allow a user to use this command even when the directory might already exist, rather than having to perform test -d to decide whether to use this function or just cd (which completely negates its usefulness!).

While we're looking at more options, also consider allowing -- to signal end of options.

echo '…' >&2 && return 1

I think these returns should be unconditional. There's no good reason to continue just because the error reporting failed.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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