Bin CLI is a simple task runner, designed to be used in code repositories, with scripts written in any programming language.
It automatically searches in parent directories, so you can run scripts from anywhere in the project tree. It also supports aliases, unique prefix matching and tab completion, reducing the amount you need to type.
It is implemented as a self-contained Bash script, small enough to bundle with your dotfiles or projects if you want to. It only requires Bash 3+ and a small number of coreutils commands, so it should work on almost any Unix-like system (Linux, macOS, etc.). On Windows, it can be used via WSL, Git Bash, Cygwin or MSYS2.
Collaborators / contributors who choose not to install Bin can run the scripts directly, so you can enjoy the benefits without adding a hard dependency or extra barrier to entry.
To see how Bin compares to some of the alternatives (Just, Task, Make, etc.), see the wiki.
A project just needs a bin/
folder and some executable scripts - for example:
repo/
├── bin/
│ ├── build
│ ├── deploy
│ └── hello
└── ...
The scripts can be written in any language,
or can even be compiled binaries, as long as they are executable (chmod +x
). Here is a very simple
bin/hello
shell script:
#!/bin/sh
echo "Hello, ${1:-World}!"
To execute it, run:
$ bin hello
Hello, World!
Now you may be thinking why not just run it directly, like this:
$ bin/hello
And that would do the same thing - but Bin will also search in parent directories, so you can use it from anywhere in the project:
$ cd app/Http/Controllers/
$ bin/hello # Doesn't work :-(
$ ../../../bin/hello # Works, but is rather tedious to type :-/
$ bin hello # Still works :-)
Warning
Bin CLI executes arbitrary commands/scripts in the current working directory
(or the directory specified by --dir
) - the same as if you executed them
directly. You should not run commands from untrusted sources.
If you run bin
on its own, it will list all available commands:
$ bin hel Available Commands bin build bin deploy bin hello
Can I add descriptions to the commands?
Yes - see Help text, below.
If you have multiple related commands, you may want to group them together and make subcommands. To do that, just create a subdirectory:
repo/
└── bin/
└── deploy/
├── production
└── staging
Now bin deploy production
will run bin/deploy/production
, and bin deploy
will list the available subcommands:
$ bin deploy Available Subcommands bin deploy production bin deploy staging
Any unique prefix is enough to run a command - so if bin/hello
is the only script starting with h
, all of these will work too:
$ bin hell
$ bin hel
$ bin he
$ bin h
This also works with subcommands - e.g. bin dep prod
might run bin/deploy/production
.
If you type a prefix that isn't unique, Bin will display a list of matches instead:
$ bin hel Matching Commands bin hello bin help
How can I disable unique prefix matching?
If you prefer to disable unique prefix matching, use
--exact
on the command line:bin --exact helloYou'll probably want to set up a shell alias rather than typing it manually:
alias bin='bin --exact'To disable it for a project, add this at the top of
.binconfig
:exact = true
To enable it again, overriding the config file, use
--prefix
:bin --prefix helAgain, you'll probably want to set up a shell alias:
alias bin='bin --prefix'
If you are using Ubuntu, you can install Bin CLI from the official PPA package:
sudo add-apt-repository ppa:bin-cli/bin-cli
sudo apt install bin-cli
This includes man pages (man bin
, man binconfig
) and tab completion.
Bin CLI is a single script that you can simply download to anywhere in your $PATH
.
To install it system-wide (for all users) in /usr/local/bin
:
sudo wget https://github.com/bin-cli/bin-cli/releases/latest/download/bin -O /usr/local/bin/bin
sudo chmod +x /usr/local/bin/bin
To install it for the current user only in $HOME/.local/bin
:
mkdir -p ~/.local/bin
wget https://github.com/bin-cli/bin-cli/releases/latest/download/bin -O ~/.local/bin/bin
chmod +x ~/.local/bin/bin
# If $HOME/.local/bin is not already in your $PATH:
echo 'PATH="$HOME/.local/bin:$PATH"' >> ~/.profile
PATH="$HOME/.local/bin:$PATH"
What are the system requirements?
The requirements are minimal:
- Bash 3.x or above
- Core Utilities, BusyBox or equivalent (specifically
basename
,chmod
,dirname
,mkdir
,readlink
,sort
,tr
,uniq
)
What if wget
is not available?
You can use
curl
instead:sudo curl https://github.com/bin-cli/bin-cli/releases/latest/download/bin -Lo /usr/local/bin/bin curl https://github.com/bin-cli/bin-cli/releases/latest/download/bin -Lo ~/.local/bin/bin
At least one of
curl
orwget
are usually installed, or can easily be installed, so that covers 99.99% of cases... But just for completeness - you can also use HTTPie:sudo http get https://github.com/bin-cli/bin-cli/releases/latest/download/bin -do /usr/local/bin/bin http get https://github.com/bin-cli/bin-cli/releases/latest/download/bin -do ~/.local/bin/bin
Or Node.js:
sudo npx download-cli https://github.com/bin-cli/bin-cli/releases/latest/download/bin -o /usr/local/bin/ npx download-cli https://github.com/bin-cli/bin-cli/releases/latest/download/bin -o ~/.local/bin/
Or just click this link to download it using your browser.
To upgrade to the latest version at any time, just repeat the same wget
command as above.
You may want to watch this repo to be notified when a new version is released - select Watch > Custom > Releases (or Watch > All Activity if you prefer).
To enable tab completion in Bash, add this:
command -v bin &>/dev/null && eval "$(bin --completion)"
To any of the following files:
/usr/share/bash-completion/completions/bin
(recommended for system-wide installs)/etc/bash_completion.d/bin
~/.local/share/bash-completion/completions/bin
(recommended for per-user installs)~/.bash_completion
~/.bashrc
How to use tab completion with custom aliases?
If you are using a simple shell alias, e.g.
alias b=bin
, update the filename to match and add--exe <name>
:# e.g. in /usr/share/bash-completion/completions/b command -v bin &>/dev/null && eval "$(bin --completion --exe b)"If you have globally disabled unique prefix matching, e.g.
alias bin='bin --exact'
, add the same parameter here:# e.g. in /usr/share/bash-completion/completions/bin command -v bin &>/dev/null && eval "$(bin --completion --exact)"Similarly, if you are using an alias with a custom script directory, e.g.
alias src='bin --dir scripts'
, add the same parameter here:# e.g. in /usr/share/bash-completion/completions/scr command -v bin &>/dev/null && eval "$(bin --completion --exe scr --dir scripts)"If you have multiple aliases, just create a file for each one (or put them all together in
~/.bash_completion
or~/.bashrc
).
Why use eval
?
Using
eval
makes it more future-proof - in case I need to change how tab completion works in the future.If you prefer, you can manually run
bin --completion
and paste the output into the file instead.
What about other shells (Zsh, Fish, etc)?
Only Bash is supported at this time. I will add other shells if there is demand for it, or gladly accept pull requests.
To download the man pages system-wide:
sudo mkdir -p /usr/local/share/man/man{1,5}
sudo wget https://github.com/bin-cli/bin-cli/releases/latest/download/bin.1.gz -O /usr/local/share/man/man1/bin.1.gz
sudo wget https://github.com/bin-cli/bin-cli/releases/latest/download/binconfig.5.gz -O /usr/local/share/man/man5/binconfig.5.gz
Or for the current user:
mkdir -p ~/.local/share/man/man{1,5}
wget https://github.com/bin-cli/bin-cli/releases/latest/download/bin.1.gz -O ~/.local/share/man/man1/bin.1.gz
wget https://github.com/bin-cli/bin-cli/releases/latest/download/binconfig.5.gz -O ~/.local/share/man/man5/binconfig.5.gz
# If $HOME/.local/share/man is not already in your $MANPATH:
echo 'MANPATH="$HOME/.local/share/man:$MANPATH"' >> ~/.profile
MANPATH="$HOME/.local/share/man:$MANPATH"
In the root of the repository, create a bin/
directory. For example:
mkdir bin
Then create some scripts inside it, in the language of your choice, using the text editor of your choice:
nano bin/sample
And make them executable:
chmod +x bin/*
That's all there is to it. Now you can run them:
bin sample
Can I change the directory name?
Yes - see custom script directory, below.
Does the bin/
directory have to exist?
No - if you define all commands inline in the config file, you can omit the
bin/
directory.You can also put the scripts in the root directory - but then subcommands won't be supported.
Some of the features below require you to create a config file. It should be named .binconfig
and placed in the project root directory, alongside the bin/
directory:
repo/
├── bin/
│ └── ...
└── .binconfig
Config files are written in INI format. Here is an example:
; Global settings
dir = scripts
exact = true
merge = true
template = #!/bin/sh\n\n
; Settings for each command (script)
[hello]
alias = hi
help = Say "Hello, World!"
[phpunit]
command = "$BIN_ROOT/vendor/bin/phpunit" "%@"
The supported global keys are:
dir
(string) - Sets a custom script directoryexact
(boolean) - Disables unique prefix matchingmerge
(boolean, or the stringoptional
) - Enables directory mergingtemplate
(string) - Sets the template for scripts created with--create
The supported per-command keys are:
alias
/aliases
(comma-separated strings) - Aliaseshelp
(string) - Help textcommand
(string) - Inline commands
Do I need to create a .binconfig
file?
No -
.binconfig
only needs to exist if you want to use the features described below.
What dialect of INI file is used?
The INI file is parsed according to the following rules:
- Spaces are allowed around the
=
signs, and are automatically trimmed from the start/end of lines.- Values should not be quoted - quotes will be treated as part of the value. This avoids the need to escape inner quotes.
- Boolean values can be set to
true
/false
(recommended),yes
/no
,on
/off
or1
/0
(all case-insensitive). Anything else triggers an error.- Lines that start with
;
or#
are comments, which are ignored. No other lines can contain comments.
Why isn't .binconfig
inside bin/
?
.binconfig
can't be inside thebin/
directory because thedir
setting may change the name of thebin/
directory, creating a chicken-and-egg problem (how would we find it in the first place?).Technically it would be possible to support both locations for every setting except
dir
- and I may if there is demand for it... But then we would have to decide what happens if there are two files - error, or merge them? If merged, how should we handle conflicts? Which one shouldbin --edit .binconfig
open? And so on.
What happens if an invalid key name is used?
Invalid keys are ignored, to allow for forwards-compatibility with future versions of Bin CLI which may support additional settings. (The downside of this is you won't be warned if you make a typo, so I may change this in the future.)
Invalid command names are displayed as a warning when you run
bin
, after the command listing.
You can use these commands to more easily create/edit scripts in your preferred editor ($VISUAL
or $EDITOR
, with editor
, nano
or vi
as fallbacks):
bin --create sample
bin --edit sample
The --create
(-c
) command will pre-fill the file with a typical Bash script template and make it executable.
The --edit
(-e
) command supports unique prefix matching (e.g. bin -e sam
).
You can also use bin --create .binconfig
to create a config file, and bin --edit .binconfig
to edit it.
How can I customise the template for new scripts?
Add this to the top of
.binconfig
:template = #!/usr/bin/env bash\nset -euo pipefail\n\nIt is passed to
echo -e
, so you can use escape sequences such as\n
for new lines.
To add a short (one-line) description of each command, enter it in .binconfig
as follows:
[deploy]
help = Sync the code to the live server
This will be displayed when you run bin
with no parameters (or with an ambiguous prefix). For example:
$ bin Available Commands bin artisan Run Laravel Artisan with the appropriate version of PHP bin deploy Sync the code to the live server bin php Run the appropriate version of PHP for this project
I recommend keeping the descriptions short. The scripts could then support a --help
parameter, or similar, if further explanation is required.
For subcommands, use the full command name, not the filename:
[deploy live]
help = Deploy to the production site
[deploy staging]
help = Deploy to the staging site
You can define aliases in .binconfig
like this:
[deploy]
alias = publish
This means bin publish
is an alias for bin deploy
, and running either would execute the bin/deploy
script.
You can define multiple aliases by separating them with commas (and optional spaces). You can use the key aliases
if you prefer to be pedantic:
[deploy]
aliases = publish, push
Or you can list them on separate lines instead:
[deploy]
alias = publish
alias = push
Alternatively, you can use symlinks to define aliases:
$ cd bin
$ ln -s deploy publish
Be sure to use relative targets, not absolute ones, so they work in any location. (Absolute targets will be rejected, for safety.)
In any case, aliases are listed alongside the help text when you run bin
with no parameters (or with a non-unique prefix). For example:
$ bin Available Commands bin artisan Run Laravel Artisan with the appropriate version of PHP (alias: art) bin deploy Sync the code to the live server (aliases: publish, push)
Can I define aliases for commands that have subcommands?
Yes - for example, given a script
bin/deploy/live
and this config file:[deploy] alias = push
bin push live
would be an alias forbin deploy live
, and so on.
How do aliases affect unique prefix matching?
Aliases are checked when looking for unique prefixes. In this example:
[deploy] aliases = publish, push
bin pub
would matchbin publish
, which is an alias forbin deploy
, which runs thebin/deploy
scriptbin pu
would match bothbin publish
andbin push
- but since both are aliases forbin deploy
, that would be treated as a unique prefix and would therefore also runbin/deploy
What happens if an alias conflicts with another command?
Defining an alias that conflicts with a script or another alias will cause Bin to exit with error code 246 and print a message to stderr.
If you have a really short script, you can instead write it as an inline command in .binconfig
:
[hello]
command = echo "Hello, ${1:-World}!"
[phpunit]
command = "$BIN_ROOT/vendor/bin/phpunit" "$@"
[watch]
command = "$BIN_DIR/build" --watch "$@"
The following variables are available:
$1
,$2
, ... and$@
contain the additional arguments, as normal$BIN_ROOT
points to the project root directory (where.binconfig
is found)$BIN_DIR
points to the directory containing the scripts (usually$BIN_ROOT/bin
, unless configured otherwise)- The standard environment variables listed below
How complex can the command be?
The command is executed within a Bash shell (
bash -c "$command"
), so it may contain logic operators (&&
,||
), multiple commands separated by;
, and pretty much anything else that you can fit into a single line. Multi-line commands are not supported.
Why is this not the standard / recommended way to write commands?
If you're using Bin as a replacement for the one-line tasks typically defined in package.json, it might seem perfectly natural to write all tasks this way (and you can do that if you want to).
However, I generally recommend writing slightly longer, more robust scripts. For example, checking that dependencies are installed before you attempt to do something that requires them, or even installing them automatically. It's hard to do that when you're limited to a single line of code.
It also violates this fundamental principle of Bin, listed in the introduction above:
Collaborators / contributors who choose not to install Bin can run the scripts directly, so you can enjoy the benefits without adding a hard dependency or extra barrier to entry.
That's why I recommend only using inline commands for very simple commands, such as calling a third-party script installed by a package manager (as in the
phpunit
example) or creating a shorthand for a command that could easily be run directly (as in thewatch
example).
You can create scripts with an extension to represent the language, if you prefer that:
repo/
└── bin/
├── sample1.sh
├── sample2.py
└── sample3.rb
The extensions will be removed when listing commands and in tab completion (as long as there are no conflicts):
$ bin Available Commands bin sample1 bin sample2 bin sample3
You can run them with or without the extension:
$ bin sample1
$ bin sample1.sh
You must include the extension in .binconfig
:
[sample1.sh]
help = The extension is required here
If you prefer the directory to be named scripts
(or something else), you can configure that at the top of .binconfig
:
dir = scripts
The path is relative to the .binconfig
file - it won't search any parent or child directories.
This option is provided for use in projects that already have a scripts
directory or similar. I recommend renaming the directory to bin
if you can, for consistency with the executable name and standard UNIX naming conventions.
Can I put the scripts in the project root directory?
If you have your scripts directly in the project root, you can use this:
dir = .
However, subcommands will not be supported, because that would require searching the whole (potentially very large) directory tree to find all the scripts.
What if I can't create a config file?
You can also set the script directory at the command line:
$ bin --dir scriptsBin will search the parent directories as normal, but ignore any
.binconfig
files it finds. This is mostly useful to support repositories you don't control.You will probably want to define an alias:
alias scr='bin --exe scr --dir scripts'
Can I use an absolute path?
Not in a
.binconfig
file, but you can use an absolute path at the command line. For example, you could put your all generic development tools in~/.local/bin/dev/
and run them asdev <script>
:alias dev="bin --exe dev --dir $HOME/.local/bin/dev"
I often use Bin to create shims for other executables - for example, different PHP versions or running scripts inside Docker.
Rather than typing bin php
every time, I use a Bash alias to run it automatically:
alias php='bin php'
However, that only works when inside a project directory. The --shim
parameter tells Bin to run the global command of the same name if no local script is found:
alias php='bin --shim php'
Now typing php -v
will run bin/php -v
if available, but fall back to a regular php -v
if not.
If you want to run a fallback command that is different to the script name, use --fallback <command>
instead:
alias php='bin --fallback php8.1 php'
Both of these options imply --exact
- i.e. unique prefix matching is disabled (otherwise it might call bin/phpunit
, for example).
Bin will set the environment variable $BIN_COMMAND
to the command that was executed, for use in help messages:
echo "Usage: ${BIN_COMMAND-$0} [...]"
For example, if you ran bin sample -h
, it would be set to bin sample
, so would output:
Usage: bin sample [...]
But if you ran the script manually with bin/sample -h
, it would output the fallback from $0
instead:
Usage: bin/sample [...]
There is also $BIN_EXE
, which is set to the name of the executable (typically just bin
, but that may be overridden).
If you prefer to shorten the script prefix from bin
to b
, for example, you can create an alias in your shell's config. For example, in ~/.bashrc
:
alias b='bin --exe b'
The --exe
parameter is used to override the executable name used in the environment variables ($BIN_COMMAND
, $BIN_EXE
) and the list of commands:
$ b Available Commands b hello
You can skip it (i.e. use alias b='bin'
) if you prefer it to say bin
.
Alternatively, you can use a symlink
System-wide installation:
$ sudo ln -s bin /usr/local/bin/bPer-user installation:
$ ln -s bin ~/.local/bin/b
Occasionally, you may want to define commands that are specific to a certain subdirectory, without losing access to the main (parent) project commands.
For example, you may have several different themes, each with its own build
command:
repo/ ← parent project
├── bin/
│ └── deploy
└── themes/
└── one/ ← child project
├── bin/
│ └── build
└── .binconfig
Normally, if you are in the themes/one/
directory:
bin build
runsthemes/one/bin/build
bin deploy
gives an error, because the parent directory is ignored
But if you add this to .binconfig
(in the child project):
merge = true
Then the two bin/
directories are merged, so:
bin build
still runsthemes/one/bin/build
bin deploy
runsbin/deploy
Can child project commands override parent project commands?
No - any conflicts will be reported as an error, the same as if they were defined at the same level (e.g. by defining a command and an alias with the same name).
This is mostly because it would make the conflict-checking code too complex - but it has the benefit of enforcing simplicity (parent commands work from anywhere, and accidental conflicts are reported).
Does this work with inline commands and aliases?
Yes - you can use any combination of scripts, inline commands and aliases in both the parent and child projects.
What if no parent project is found?
If you set
merge = true
but there is no parentbin/
directory (or.binconfig
file), Bin will exit with an error.To avoid that, set
merge = optional
instead. This may be useful in subprojects that have separate repositories, so you can't guarantee they will be cloned together.
Can three (or more) directories be merged?
Yes - just set
merge = true
at each level below the first.
Scripts starting with _
(underscore) are excluded from listings, but can still be executed. This can be used for hidden tools and helper scripts that are not intended to be executed directly. (Or you could use a separate libexec
directory in the project root if you prefer.)
Files starting with .
(dot / period) are always ignored and cannot be executed with Bin.
Files that are not executable (not chmod +x
) are listed as warnings in the command listing, and will error if you try to run them. The exception is when using dir = .
, where they are just ignored.
A number of common non-executable file types (*.json
, *.md
, *.txt
, *.yaml
, *.yml
) are also excluded when using dir = .
, even if they are executable, to reduce the noise when all files are executable (e.g. on FAT32 filesystems).
The directories /bin
, /snap/bin
, /usr/bin
, /usr/local/bin
, $HOME/bin
and $HOME/.local/bin
are ignored when searching parent directories, unless there is a corresponding .binconfig
file, because they are common locations for global executables (typically in $PATH
).
Usage: bin [OPTIONS] [--] [COMMAND] [ARGUMENTS...]
Options that can be used with a command:
--dir DIR Specify the directory name to search for (overrides .binconfig)
--exact Disable unique prefix matching
--exe NAME Override the executable name displayed in the command list
--fallback COMMAND If the command is not found, run the given global command (implies '--exact')
--prefix Enable unique prefix matching (overrides .binconfig)
--shim If the command is not found, run the global command with the same name (implies '--exact')
Options that do something with a COMMAND:
--create, -c Create the given script and open in your $EDITOR (implies '--exact')
--edit, -e Open the given script in your $EDITOR
Options that do something special and don't accept a COMMAND:
--completion Output a tab completion script for the current shell
--info Display information about the current project (root, bin directory and config file location)
--help, -h Display this help
--version, -v Display the current version number
Any options must be given before the command, because everything after the command will be passed as parameters to the script.
For more details see https://github.com/bin-cli/bin-cli/tree/main#readme