Mistakes I've made treating file paths as strings

Phil Nash - Mar 5 '20 - - Dev Community

Some things you do as a developer can work for you for years, then turn around and bite you when you were least expecting. These are the things that you wish another developer had told you early in your career so you never had to make the mistakes. This post is about one of those things and if you’re reading this, consider it me telling you.

File paths look like strings. You have a number of directories and maybe a file name with an extension at the end. You separate the directories and files with a / character and the result looks like /path/to/file. So you can treat them like strings, joining them or concatenating them until you pass them to another file method that is used to read from or write to the file. These were my thoughts from just a few months ago. Here’s where I was wrong.

Don’t forget Windows

If you develop on a Mac, like I have the privilege of doing, or Linux then you might have read the above paragraph and not noticed anything wrong. If develop on Windows you probably sighed into your cup of coffee as you read the / character.

It’s all too easy to forget when you work with a Mac and deploy to Linux environments, like I have done for years, that Windows uses backslashes. It’s all too painful to find out you’ve made that mistake when you work on a command line tool that needs to run on both types of platform. create-twilio-function is one such command line tool that had to go through a number of changes from things like:

mkdir(path + '/' + dirName);
Enter fullscreen mode Exit fullscreen mode

to

const path = require('path');
mkdir(path.join(pathName, dirName));
Enter fullscreen mode Exit fullscreen mode

so that it would work properly on Windows.

To Windows users, I’m sorry. To everyone else, when working with Node.js the path module is your friend. Use path.join whenever you have to join two paths. And check out other utilities like path.relative, which returns a relative path from one path to another, and path.normalize, which returns a path resolving segments like . or ...

Pay no attention to path.sep, which returns a / or a \ depending on the system you’re working on, just use path.join.

Paths behave differently to strings

To my second mistake, this time working in Ruby. This one was slightly more subtle and evaded my tests. You see, you can use the Pathname class to create fragments of paths and then concatenate them. For example:

require "pathname"
path1 = Pathname.new("path")
path2 = Pathname.new("to")
path1 + path2
# => #<Pathname:path/to>
Enter fullscreen mode Exit fullscreen mode

As you can see Pathname objects have a + operator that concatenates the paths, much like + concatenates strings. In fact, it also works with a mix of strings and paths:

require "pathname"
path1 = Pathname.new("path")
path2 = "to"
path1 + path2
# => #<Pathname:path/to>
Enter fullscreen mode Exit fullscreen mode

This all seems well and good, except it doesn’t work the other way around.

require "pathname"
path1 = "to"
path2 = Pathname.new("path")
path1 + path2
# => TypeError (no implicit conversion of Pathname into String)
Enter fullscreen mode Exit fullscreen mode

A nice error like that means we’ve done something wrong, that was not the problem I had though. No, the issue I had stemmed from expecting to concatenate a pathname and a string and instead concatenating two strings. This manifested itself in my Rubygem jekyll-gzip. You see, I was trying to create a glob of paths with the line:

files = Dir.glob(dir + "**/*{#{extensions}}")
Enter fullscreen mode Exit fullscreen mode

It turned out under some circumstances dir was actually a string instead of a pathname and it didn’t include a separator. So the glob was looking for "dirname **/*{#{extensions}}" when I really wanted it to look for "dirname/** /*{#{extensions}}". Concatenating two pathnames or a pathname and a string will add the separator (as someone pointed out in a comment on my commit), but concatenating two strings will not. This meant that the gem happily went looking for the wrong pathname, found no files and then proceeded to successfully do nothing. Replacing the entire line with:

files = Dir.glob(File.join(dir, "**", "*{#{extensions}}"))
Enter fullscreen mode Exit fullscreen mode

fixed the issue. In this case File.join is the method to use to avoid surprises with strings.

Always use the built in path tools

Whether you’re working in Node.js, Ruby, or any other language do not be tempted to treat file paths as strings. They behave differently on different platforms and mixing paths and strings together can cause hard to debug errors.

Use your standard library and save yourself the hassle.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .