• Stars
    star
    704
  • Rank 64,316 (Top 2 %)
  • Language
    Shell
  • License
    MIT License
  • Created almost 3 years ago
  • Updated 8 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

a cozy nest for your scripts

sd: my script directory

Has this ever happened to you?

Black and white video plays of someone struggling to find a shell script they wrote a year ago and stuffed into their ~/bin without giving it a very meaningful name.

Don't you hate it when you can't find the scripts you need, when you need it? Well now there's a better way!

Color fills the screen. Someone holds sd up to the camera, and flashes a winning smile. They've found the script on their first try.

Introducing sd, the script directory for the refined, sophisticated professional. Simply organize your scripts in a logical directory hierarchy, and let sd take care of the rest!

$ tree ~/sd
/Users/ian/sd
β”œβ”€β”€ blog
β”‚   β”œβ”€β”€ edit
β”‚   β”œβ”€β”€ preview
β”‚   └── publish
β”œβ”€β”€ nix
β”‚   β”œβ”€β”€ diff
β”‚   β”œβ”€β”€ info
β”‚   └── sync
└── tmux
    └── init

And now instead of typing ~/sd/blog/publish, you can just type sd blog publish -- a savings of nearly three whole characters!

But wait! There's more! You'll wonder how you ever lived without sd's best-in-class tab completion:

$ sd nix <TAB>
diff  -- prints what will happen if you run sync
info  -- <package> prints package description
sync  -- make user environment match ~/dotfiles/user.nix

Simply write a one-line comment in your script, and you'll never be left scratching your head over how you were supposed to call it!

uhh

Hi okay sorry. Take a look at this blog post for a real introduction and a fancy asciinema demo of how it works.

Usage

The default behavior for sd foo bar is:

  • If ~/sd/foo is an executable file, execute ~/sd/foo bar.
  • If ~/sd/foo/bar is an executable file, execute it with no arguments.
  • If ~/sd/foo/bar is a directory, this is the same is sd foo bar --help (it prints usage information).
  • If ~/sd/foo/bar is a non-executable regular file, this is the same is sd foo bar --cat (it just prints the file out).

There are some special flags that are significant to sd. If you supply any one of these arguments, sd will not invoke your script, and will do something fancier instead.

$ sd foo bar --help
$ sd foo bar --new
$ sd foo bar --edit
$ sd foo bar --cat
$ sd foo bar --which
$ sd foo bar --really

--help

Print the contents of a help file, or generate a help file from comments in a script.

For executables, sd looks for a file with the same name but the .help extension. For example, sd nix diff --help would look for a file called ~/sd/nix/diff.help, and print it out.

For directories, sd looks for a file that's just called help. So sd nix --help would look for ~/sd/nix/help.

If there is no help file for an executable, sd will print the first comment block in the file instead. sd currently only recognizes bash-style # comments.

For example:

$ cat ~/sd/nix/sync
#!/usr/bin/env bash

# make user environment match ~/dotfiles/user.nix
#
# This will remove any packages you've installed with nix-env
# but have not added to user.nix. To see exactly what this
# will do, run:
#
#     sd nix diff

set -euo pipefail

# maybe this should be configurable
nix-env -irf ~/dotfiles/user.nix

That will produce the following help output (note that it only prints the first contiguous comment block):

$ sd nix sync --help
make user environment match ~/dotfiles/user.nix

This will remove any packages you've installed with nix-env
but have not added to user.nix. To see exactly what this
will do, run:

    sd nix diff

If you run --help for a directory, it will also print out a command listing after the help text:

$ sd nix --help
nix commands

install    -- <package> use --latest to install from nixpkgs-unstable
shell      -- add gcroots for shell.nix
diff       -- prints what will happen if you run sync
info       -- <package> prints package description
sync       -- make user environment match ~/dotfiles/user.nix

--new

Everything to the left of --new is considered a command path, and everything to the right of --new is considered the command body. For example:

$ sd foo bar --new echo hi

Will try to create a new command at ~/sd/foo/bar with an initial contents of echo hi.

Actually, to be more precise, it will create this script:

$ cat ~/sd/foo/bar
#!/usr/bin/env bash

set -euo pipefail

echo hi

Assuming the default template.

If no body is supplied after --new, sd will open the script for editing.

custom script templates

You can customize the template used by --new by creating a file called template, either in ~/sd or one of its subdirectories.

sd will try to find a template by walking recursively up the directory hierarchy. For example, if you run:

$ sd foo bar baz --new

sd will try to find a template at ~/sd/foo/bar/template first, then fall back to ~/sd/foo/template, then ~/sd/template. If it doesn't find any template file, it will use the default bash template shown above.

(There is no need to make your template executable -- sd will take care of that for you.)

When --new is used to create an inline script, that script will always go at the end of your template file. There is currently no way to customize this.

--cat

Prints the contents of the script. See SD_CAT below.

--edit

Open the script in an editor. See SD_EDITOR below.

--which

Prints the path of the script.

--really

Suppress special handling of all of the other special flags. This allows you to pass --help or --new as arguments to your actual script, instead of being interpreted by sd. For example:

$ sd foo bar --help --really

Will invoke:

~/sd/foo/bar --help

The first occurrence of the --really argument will be removed from the arguments passed to the script, so if you need to pass a literal --really, you must pass it twice to sd. For example:

$ sd foo bar --help --really --really

Will invoke:

$ ~/sd/foo/bar --help --really

Context

When a script is invoked, sd will set the environment variable SD to the directory that the script was found in -- in other words, $(dirname "$0").

This makes it slightly more convenient to refer to shared helper files or other scripts relative to the executing script.

Options

sd respects some environment variables:

  • SD_ROOT: location of the script directory. Defaults to $HOME/sd.
  • SD_EDITOR: used by sd foo --edit and sd foo --new. Defaults to $VISUAL, then $EDITOR, then finally falls back to vi if neither of those are set.
  • SD_CAT: program used when printing files, in case you want to use something like bat. Defaults to cat.

Installation

There are two ways to use sd:

  1. source the sd file, which will define the shell function sd
  2. treat sd as a regular executable and put it somewhere on your PATH

I prefer to use sd as a regular executable, but the function approach is more convenient if you already use a shell plugin manager that knows how to set up fpath automatically.

Note that you cannot invoke "recursive sd" (that is, write scripts that themselves invoke sd) if you use the function approach, unless you're writing zsh scripts. But you probably shouldn't.

Installation as a regular script

Using Nix

As far as I know, Nix is the only package manager with sd pre-packaged (as nixpkgs.service-directory).

sd is also available in home manager. You can install it by adding something like this to your ~/.config/home-manager/home.nix:

{...}: {
  home.programs.script-directory = {
    script-directory = {
      enable = true;
      settings = {
        # SD_ROOT = "${config.home.homeDirectory}/custom-script-directory";
        # SD_EDITOR = "vim";
        # SD_CAT = "bat";
      };
    };
  };
  
  home.programs.zsh = {
    # The script-directory module doesn't automatically configure
    # zsh completion, so we still have manually add this:
  
    initExtra = ''
    fpath+="${pkgs.script-directory}/share/zsh/site-functions"
    '';
  };
}

Without a package manager

  1. Put the sd script somewhere on your PATH.
  2. Put the _sd completion script somewhere on your fpath.

I like to symlink sd to ~/bin, which is already on my path. If you've cloned this repo to ~/src/sd, you can do that by running something like:

$ ln -s ~/src/sd/sd ~/bin/sd

There isn't really a standard place in your home directory to put completion scripts, so unless you've made your own, you'll probably want to add your clone directly to your fpath. You should add that to your .zshrc file before the line where you call compinit. It should look something like this:

# ~/.zshrc

fpath=(~/src/sd $fpath)
autoload -U compinit
compinit

If you use a zsh framework like oh-my-zsh, it probably calls compinit for you. In that case, just set your fpath before you source the framework's initialization script.

Note that changes you make to your ~/.zshrc will only take effect for future shells you create, so to start enjoying sd immediately you'll also want to run these commands in your existing shells:

$ fpath=(~/src/sd $fpath)
$ compinit

As a shell function

You can just source sd in your .zshrc and set up completion manually (as described above), but sd is designed to be compatible with shell plugin managers.

Antigen

Add this line to your .zshrc:

antigen bundle ianthehenry/sd

oh-my-zsh:

Clone this repo into your custom plugins directory:

$ git clone https://github.com/ianthehenry/sd.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/sd

And then add it to the plugins list in your ~/.zshrc before you source oh-my-zsh:

plugins+=(sd)
source "$ZSH/oh-my-zsh.sh"

bash/fish autocompletion support

Patrick Jackson contributed an unofficial fish completion script, which should be usable with some modification (as written it does not respect SD_ROOT, but it should act as a very good starting point if you use fish).

Bash doesn't support the fancy completion-with-description feature that is sort of the whole point of sd, but there are apparently ways to hack something similar.

Changelog

v1.1.0 2022-10-30

  • fix a bug where --help would print every comment in the script

v1.0.1 2022-04-17

  • better error message if ~/sd does not exist
  • better error message if ~/sd exists but is not a directory

v1.0.0 2022-02-27

sd is now released under the MIT license. There are no functional changes from the pre-1.0 releases.

v0.3.0 2022-02-26

  • scripts now run with the SD environment variable set to the directory they were found in
  • autocompletion now completes arguments to commands instead of just commands
    • only completes positional file arguments and the built-in flags (like --help)
  • sd now only forks a subshell when invoked as a function
  • sd now execs scripts instead of fork+exec
    • this fixes the rare issue where a long-running script could throw errors when it finished if you were editing the sd executable itself while the script was running, because bash was trying to execute the "rest" of the file and apparently doing so by byte index or something (??)
    • this only affects me

v0.2.0 2022-02-24

  • added per-directory template files, to override the bash default

v0.1.1 2021-12-05

  • fix a bug where --new wouldn't work unless provided with an initial script

v0.1.0 2021-12-01

  • added --really
  • dir.help files are now dir/help files

You used to be able to provide a description for a directory called foo/ by writing a file called foo.help as a sibling of that directory.

Now directory help summaries are expected in foo/help instead.

This has the sort-of nice effect that sd foo help is sometimes similar to sd foo --help. Except that the latter also prints out subcommands.

More Repositories

1

bauble

a playground for making 3D art with lisp and math
Janet
378
star
2

zsh-autoquoter

automatically quote arguments to commands like `git commit -m`
Shell
114
star
3

basilica

It's kinda like a forum.
Haskell
114
star
4

mixologician

optimize your home bar with ✨logic programming✨
Prolog
75
star
5

judge

self-modifying test library for janet
Janet
68
star
6

toodle.studio

turtle graphics playground
TypeScript
66
star
7

dotfiles

My dotfiles. For easy clonin'.
Emacs Lisp
49
star
8

macaroni

macro spaghetti code
Janet
28
star
9

ocamlsyntax.com

How do you do that recursive GADT thing again?
CSS
27
star
10

cmd

command-line argument parser for Janet
Janet
24
star
11

jimmy

Janet bindings for the persistent data structure library "immer"
C++
18
star
12

tightrope

Making Slackbots is hard! But wait: now it's easy.
Haskell
14
star
13

janet-stuff

an index of all of my public-facing janet projects
11
star
14

pat

a better pattern matching macro for janet
Janet
10
star
15

sd-nix

some helpers for working with nix
Shell
10
star
16

privy

inline output for the ivy array language
Janet
9
star
17

effing.js

Function functions.
CoffeeScript
9
star
18

bytemap

text-based canvas
Janet
9
star
19

basilica-client

An Om client for basilica
Clojure
8
star
20

janet-cross-compile

demonstration of using zig cc to cross-compile native janet binaries
C
8
star
21

codemirror-lang-janet

Janet support for CodeMirror 6
TypeScript
7
star
22

memegen

A meme generator for discriminating meme enthusiasts
Haskell
7
star
23

banquet

fancy tables
Janet
7
star
24

aoc-2023

advent of code 2023 in janet
Janet
6
star
25

square

alternate PEG syntax for Janet
Janet
5
star
26

petal

my pet array language
Rust
5
star
27

janet-midi

Janet bindings for RtMidi, a cross-platform MIDI library
C++
5
star
28

janet-module

a simple module macro
Janet
5
star
29

utf8-parser

A fun way to learn about UTF-8 and parser combinators
Haskell
5
star
30

janet-clipper

Janet bindings for clipper, a polygon utility library
C++
3
star
31

to-do

a very simple plain-text todo list
Janet
3
star
32

zsh-expander

predictable fzf-based completion for zsh
Shell
3
star
33

cmp

comparison combinators for Janet
Janet
3
star
34

rogue

A roguelike, in Haskell
Haskell
3
star
35

janet-whereami

a janet wrapper around the whereami library
C
2
star
36

matchbook

Declarative pattern matching for JavaScript
CoffeeScript
2
star
37

heap

comparator-based min-max heap for janet
Janet
2
star
38

httprintf

Some scripts for creating a development HTTP server.
Shell
2
star
39

steno

command-line snapshot testing tool
Janet
2
star
40

spoon

command line interface for lambda soup
OCaml
2
star
41

territory

An abstract game of territory control made in a single sitting
Objective-C
1
star
42

nix-zshell

Hack to make nix-shell play nicely with zsh
Shell
1
star
43

brinks

Algebraic data types for JavaScript
JavaScript
1
star
44

privy-mode

emacs major mode for the ivy array language and privy preprocessor
Emacs Lisp
1
star
45

overtone-demo

A demo of overtone. For a tech talk.
Clojure
1
star
46

dismissive-clojure

Send emails... to the future!
Clojure
1
star
47

ctrl-c

Being interrupted as a service
Shell
1
star
48

inline

Inline: offline outlining.
JavaScript
1
star
49

rundown

running with markdown
Rust
1
star
50

ianthehenry

1
star
51

dismissive

Emailing the future as a service
Haskell
1
star
52

tales

An adventure in collaborative storytelling.
Haskell
1
star