Skip to main content

Command Palette

Search for a command to run...

Your Script Doesn't Need a Project

One shebang to run them all

Published
9 min read
N

Software Engineer @marmelasoft who likes to learn all kinds of crap. Interested in Elixir, Phoenix and LiveView 🔥

Setting up a new project just to run ten lines of code has always been one of programming's most tedious rituals. Create a directory. Write a manifest. Declare your dependencies. Run the install. Now you can write your actual code. It's no wonder we keep reaching for bash - it's right there, no setup, no ceremony.

Bash asks for nothing, and that's its charm. However, the moment your script gets more complex, the standard features you are used to, like for loops and data structures, are not so ergonomic in that environment. LLMs have made bash easier to produce, but no easier to trust, they hallucinate flags, confuse [ with [[, and hand you scripts that work on Linux and fail silently on macOS.

That said, a quiet revolution has been spreading across programming languages: the ability to embed your dependencies directly in a single source file and run it with one command. No project scaffolding. No manifest files.

Let's see this in action with a genuinely useful example, implemented across four languages, but many more allow this same pattern.

The Task

You're working on a web app locally. You want to test it on your phone over Wi-Fi. You could squint at the ip addr output, type an IP address on a tiny keyboard, and mistype it three times. Or you could run a script that detects your local IP, builds a URL, and prints a QR code right in the terminal you have already open. Point your phone camera at it, tap, done. The first step is super easy, just some bash shenanigans: ip -brief addr show | awk '/^wlp.*UP/{print $3}' | cut -d/ -f1. The QR part is not so simple.

This is a perfect showcase for single-file scripts with inline dependencies, something you can use and share with your colleagues just as easily.

Python (with uv)

uv is a Python package manager from Astral that, among many things, supports PEP 723 - inline script metadata. You declare your dependencies in a comment block at the top of the file, and uv handles the rest.

#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "qrcode",
# ]
# ///

import os
import socket
import qrcode

port = int(os.environ.get("PORT", 8000))

def get_local_ip():
    # ip = socket.gethostbyname(socket.gethostname()) returns 172.0.0.1 which is not what we want
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        s.connect(("8.8.8.8", 53))
        return s.getsockname()[0]
    finally:
        s.close()

url = f"http://{get_local_ip()}:{port}"

qr = qrcode.QRCode(border=1)
qr.add_data(url)
qr.print_ascii(invert=True)
print(f"\n{url}\n")

Run it:

chmod +x showurl.py
./showurl.py

uv reads the metadata block, creates an isolated environment, installs the qrcode package, caches everything, and runs the script. The first run takes a second or two. Subsequent runs are nearly instant.

Rust (with cargo script)

Rust is adopting this pattern officially via RFC 3424. You write a single .rs file with an embedded Cargo.toml in a frontmatter block:

#!/usr/bin/env -S cargo +nightly -Zscript
---
[dependencies]
qr2term = "0.3"
local-ip-address = "0.6"
---

fn main() {
    let ip = local_ip_address::local_ip().unwrap();
    let port = std::env::var("PORT").unwrap_or_else(|_| "8000".into()); 
    let url = format!("http://{}:{}", ip, port);

    qr2term::print_qr(&url).unwrap();
    println!("\n{}\n", url);
}

Run it:

cargo +nightly -Zscript showurl.rs

Note: as of March 2026, cargo script is in its final comment period for stabilization and should land on stable Rust very soon. By the time you read this, you may be able to just run cargo showurl.rs or ./showurl.rs.

The first run compiles and caches the dependencies. After that, Cargo detects nothing has changed and skips recompilation.

Elixir (with Mix.install/2)

Elixir has had this capability baked in since version 1.12. Any .exs script can call Mix.install/1 at the top to pull in dependencies from Hex on the fly:

#!/usr/bin/env elixir

Mix.install([
  {:eqrcode, "~> 0.2.1"}
])

{:ok, socket} = :gen_udp.open(0, [:inet])
:gen_udp.connect(socket, ~c"8.8.8.8", 53)
{:ok, {ip, _port}} = :inet.sockname(socket)
:gen_udp.close(socket)

ip_str = ip |> Tuple.to_list() |> Enum.join(".")
url = "http://#{ip_str}:#{System.get_env("PORT") || "8000"}"

url
|> EQRCode.encode()
|> EQRCode.render()

IO.puts("\n#{url}\n")

Run it:

chmod +x showurl.exs
./showurl.exs

Mix.install/2 is particularly elegant. It's just a function call like any other in the same language you are already writing. It resolves dependencies from Hex, caches them globally, and doesn't pollute your working directory. If you change the dependency list, it re-resolves; if you don't, it's instant.

Deno (with URL-as-Import approach)

Not every language went the metadata route. Go pioneered a different idea: what if the import path is the dependency? When you write import "github.com/spf13/cobra", there is no separate dependency declaration. The import statement tells the toolchain both what to use and where to get it.

Deno took this idea further - adding reliable version pinning in the URL, registry prefixes, and a permission system.

#!/usr/bin/env -S deno run --allow-env --allow-sys
import QRCode from "npm:qrcode@1.5";

const port = Deno.env.get("PORT") || "8000";

function getLocalIp(): string {
  for (const iface of Deno.networkInterfaces()) {
    if (iface.family === "IPv4" && !iface.address.startsWith("127.")) {
      return iface.address;
    }
  }
  return "127.0.0.1";
}

const url = `http://\({getLocalIp()}:\){port}`;
const qr = await QRCode.toString(url, { type: "terminal", small: true });
console.log(qr);
console.log(`\n${url}\n`);

The dependency is resolved the moment the runtime sees the import. It can be a URL or a prefix for a registry (with the ability to specify the exact version). After all this, it even goes a step further: the script says upfront what kind of permissions it needs. What a great feature.

It's a different answer to the same question: how do you make a single file self-contained? Instead of embedding a dependency block at the top, you embed the dependency information in the imports themselves.

Who installs the tools?

Despite the surface-level differences, every language converges on the same idea: a single file is a complete, runnable program with its own dependencies.

The result is the same every time - a QR code in your terminal:

Point your phone at it, and you're on the page. No typing, no more typos.

Every example above assumes you already have the right tool installed - uv, cargo, deno, or elixir. But what if you don't? What if you want to hand someone a single file and have it just work, regardless of what's on their machine? Without a universal bootstrapper, you are stuck writing the scripts in whatever tools your project already uses, even when that language is not the best for the job.

That's where Nix comes in. Nix is a package manager, but not in the way you're used to thinking about package managers. It builds packages in isolation, addresses them by their exact contents, and can provide any combination of tools into a temporary environment without touching the rest of your system.

And because of these features, Nix can bootstrap any of these tools transparently through the shebang. Here's the Python example, wrapped so it works on any machine with just Nix installed:

#!/usr/bin/env nix-shell
#! nix-shell -I nixpkgs=channel:nixos-25.11
#! nix-shell -p uv
#! nix-shell -i "uv run"
# /// script
# requires-python = ">=3.12"
# dependencies = [
#   "qrcode",
# ]
# ///

import os
import qrcode
import socket

port = int(os.environ.get("PORT", 8000))

def get_local_ip():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        s.connect(("8.8.8.8", 53))
        return s.getsockname()[0]
    finally:
        s.close()

url = f"http://{get_local_ip()}:{port}"
qr = qrcode.QRCode(border=1)
qr.add_data(url)
qr.print_ascii(invert=True)

print(f"\n{url}\n")

The shebang tells nix-shell to fetch uv from Nixpkgs (-p uv), then use it as the interpreter (-i "uv run"). The person running this doesn't need uv or python installed - Nix provides everything.

And if the tool does not have a way to declare its dependencies inside the script, Nix has you covered. Consider the Python version without relying on uv:

#!/usr/bin/env nix-shell
#! nix-shell -I nixpkgs=channel:nixos-25.11
#! nix-shell -p python313 python313Packages.qrcode
#! nix-shell -i python

import os
import qrcode
import socket

port = int(os.environ.get("PORT", 8000))

def get_local_ip():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        s.connect(("8.8.8.8", 53))
        return s.getsockname()[0]
    finally:
        s.close()

url = f"http://{get_local_ip()}:{port}"
qr = qrcode.QRCode(border=1)
qr.add_data(url)
qr.print_ascii(invert=True)

print(f"\n{url}\n")

The same pattern works for nearly any language in the same way. Although it's worth noting you might need some workarounds for languages that use // comments instead of #, which is the case of Deno, for example.

#!/usr/bin/env nix-shell
#! nix-shell -I nixpkgs=channel:nixos-25.11
#! nix-shell -p deno
#! nix-shell -i bash
exec deno run --allow-env --allow-sys - <<'DENO'
import QRCode from "npm:qrcode@1.5";

const port = Deno.env.get("PORT") || "8000";

function getLocalIp(): string {
  for (const iface of Deno.networkInterfaces()) {
    if (iface.family === "IPv4" && !iface.address.startsWith("127.")) {
      return iface.address;
    }
  }
  return "127.0.0.1";
}

const url = `http://\({getLocalIp()}:\){port}`;
const qr = await QRCode.toString(url, { type: "terminal", small: true });
console.log(qr);
console.log(`\n${url}\n`);
DENO

A heredoc is not the prettiest solution, but it works, and it preserves the core promise: a single file and no prerequisites beyond Nix itself.

This is what makes Nix special in this context. Each language invented its own way to declare what a script depends on - Python packages, Rust crates, Hex packages, npm modules, or even binary tools! Nix solves the layer underneath: how to get the tools themselves. And because every example pins its Nixpkgs channel (nixpkgs=channel:nixos-25.11), you also pin the world. It's a universal shebang - one bootstrapping mechanism that works for every tool and will keep working.

Stop Writing Setup Instructions

Next time you need to write a small utility for your team - a log parser, a backup tool, or a QR code generator, anything that would otherwise come with a README full of setup instructions - try a different approach. Write it with a nix shebang on top, drop it in your project's bin/ folder, and move on with your life. No virtual environment to create, no instructions explaining how to set it up. The script is the setup.

The Art of Scripting

Part 1 of 1

A series about the long, messy road from bash one-liners to scripts you actually want to maintain and the tools that finally made it feel right.