Quickly associate all source code files with your editor in macOS using duti
If you’re a developer using macOS you’ve probably had the experience of double clicking, say, a .js
file in Finder only to inadvertantly launch the wrong application, rather than your editor of choice. It’s of course easy enough to change the association for a given file extension; per the Apple docs1:
- Control-click the file, then choose Get Info.
- In the Info window, click the arrow next to “Open with.”
- Click the pop-up menu, then choose the app. To open all files of this type with this app, click Change All.
However, if we want to associate all source code files with our editor, this becomes tedious. Even for a single language we might have many file extensions to go through (a file in a JavaScript project could have the extension .js
, .es
, .es6
, .cjs
, .mjs
, .jsx
, .jsm
etc.). When setting up my macOS environment I wondered if there was a way to do this via the CLI, and via Nick Ficano’s blog2 I came across duti
(TL;DR) How to associate file extensions for all programming languages with your editor
brew install duti python-yq
curl "https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml" \
| yq -r "to_entries | (map(.value.extensions) | flatten) - [null] | unique | .[]" \
| xargs -L 1 -I "{}" duti -s com.microsoft.VSCode {} all
The above associates all source code extensions known to Github’s linguist
library with VSCode. If you use a different editor, substitute com.microsoft.VSCode
with:
- Sublime Text:
com.sublimetext.3
(or, presumably,com.sublimetext.4
for the forthcoming Sublime Text 4) - Atom:
com.github.atom
- IntelliJ:
com.jetbrains.intellij
- Other: Find your editor’s bundle ID by running:
lsappinfo | grep 'bundleID="' | cut -d'"' -f2 | sort
To associate only extensions for the top ten programming languages3 (that is: JavaScript, Python, Java, TypeScript, C#, PHP, C++, C, Shell scripts and Ruby) we can instead run:
curl "https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml" \
| yq -r '{JavaScript,Python,Java,TypeScript,"C#",PHP,"C++",C,Shell,Ruby} | to_entries | (map(.value.extensions) | flatten) - [null] | unique | .[]' \
| xargs -L 1 -I "{}" duti -s com.microsoft.VSCode {} all
That’s it, any source code files will now be opened in your editor of choice. (You might see an error failed to set com.microsoft.VSCode as handler for public.html (error -54)
but this can be safely ignored4).
How it works
duti
is a simple “does one thing well” utility that can associate a file extension5 with an application (which we specify using its bundle ID) like so:
brew install duti
duti -s com.some.ApplicationBundleID .someext all
Equipped with duti
, we just need an exhaustive list of file extensions - across all common programming languages - to feed it. Finding such a list turned out to be non-trivial. In the end I took the data from Github’s linguist
library, which keeps all programming languages known to Github collated in a YAML file. The file is very exhaustive and is updated several times a week.
languages.yml
looks like this:
# ...
Erlang:
type: programming
color: "#B83998"
extensions:
- ".erl"
- ".app.src"
- ".es"
- ".escript"
- ".hrl"
- ".xrl"
- ".yrl"
filenames:
- Emakefile
- rebar.config
- rebar.config.lock
- rebar.lock
tm_scope: source.erlang
ace_mode: erlang
codemirror_mode: erlang
codemirror_mime_type: text/x-erlang
interpreters:
- escript
language_id: 104
F#:
type: programming
color: "#b845fc"
aliases:
- fsharp
extensions:
- ".fs"
- ".fsi"
- ".fsx"
tm_scope: source.fsharp
ace_mode: text
codemirror_mode: mllike
codemirror_mime_type: text/x-fsharp
language_id: 105
# ...
We’re only interested in the extensions
field for each programming language6. We’ll use yq
- a jq
wrapper which can process YAML - to extract the file extensions for all known languages:
yq "to_entries | map(.value.extensions)" languages.yml
[
// ...
[".rst", ".rest", ".rest.txt", ".rst.txt"],
null,
[".sed"],
[".wdl"]
// ...
]
Next we’ll flatten this (flatten
), remove null
values (- [null]
), and remove duplicates (unique
):
yq "to_entries | (map(.value.extensions) | flatten) - [null] | unique" languages.yml
[
// ...
".apacheconf",
".apib",
".apl",
".app.src",
".applescript",
".arc",
".arpa",
".as",
".asax",
".asc",
".asciidoc",
// ...
]
Next we add the .[]
operator, and the -r
(output raw strings) flag, to output our extensions as plaintext, one per line:
yq -r "to_entries | (map(.value.extensions) | flatten) - [null] | unique | .[]" languages.yml
# ...
.apacheconf
.apib
.apl
.app.src
.applescript
.arc
.arpa
.as
.asax
.asc
.asciidoc
# ...
Now we have something that we can pipe through to xargs
. We use -L 1
to specify that we should run our command for each line, and -I "{}"
means we’ll substitute {}
with each extension in turn:
yq -r "to_entries | (map(.value.extensions) | flatten) - [null] | unique | .[]" languages.yml \
| xargs -L 1 -I "{}" duti -s com.microsoft.VSCode {} all
Or reading languages.yml
straight from Github:
curl "https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml" \
| yq -r "to_entries | (map(.value.extensions) | flatten) - [null] | unique | .[]" languages.yml \
| xargs -L 1 -I "{}" duti -s com.microsoft.VSCode {} all
As well as being a big timesaver, I think the script above is a nice example of the Unix philosophy in action. We have four simple programs which each do one thing well:
curl
to fetch the programming languages data from thelinguist
repoyq
to process the YAML dataxargs
to run a command for each data pointduti
to set the file extension association
I’ve since come across (but not personally used) openwith
which seems to have a simpler API than duti
- h/t to Dennis Felsing’s blog post on his macOS setup.
Footnotes
- Source: Apple: Choose an app to open a file on Mac↩
- Source: Change Mac OS default file associations from the command line with duti↩
- According to Github’s 2020 State of the Octoverse survey↩
- See this issue: duti#29↩
- Or, more technically, a Uniform Type Identifier.↩
- Note that since I ignore the
.filenames
field, extensionless source code files likeMakefile
won’t be associated with your editor. You could rerun the command with.extensions
substituted in theyq
command with.filenames
if you wanted, although I haven’t tested this.↩
Loading comments...