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 {} allThe 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.4for 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 {} allThat’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 allEquipped 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 {} allOr 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 {} allAs 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:
curlto fetch the programming languages data from thelinguistrepoyqto process the YAML dataxargsto run a command for each data pointdutito 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
.filenamesfield, extensionless source code files likeMakefilewon’t be associated with your editor. You could rerun the command with.extensionssubstituted in theyqcommand with.filenamesif you wanted, although I haven’t tested this.↩

Excellent information! Thanks for sharing it with us.
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.
Source code provides us with that information which cannot be accessed easily and I must say that this information is really hard to get.
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:
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.
Your article is really good one to read and you are making a huge effort in providing real content for free and keep posting more content like these.
File "/opt/homebrew/bin/yq", line 8, in sys.exit(cli()) ^^^^^ File "/opt/homebrew/Cellar/python-yq/3.1.1/libexec/lib/python3.11/site-packages/yq/init.py", line 78, in cli parser = get_parser(program_name, doc) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/homebrew/Cellar/python-yq/3.1.1/libexec/lib/python3.11/site-packages/yq/parser.py", line 78, in get_parser parser = Parser(**parser_args) ^^^^^^^^^^^^^^^^^^^^^ TypeError: ArgumentParser.init() got an unexpected keyword argument 'allow_abbrev' 23 134k 23 32750 0 0 375k 0 --:--:-- --:--:-- --:--:-- 404k curl: (23) Failure writing output to destination
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: