Introduction
Hash is a modern, POSIX-compliant command line interpreter for Linux, macOS, and BSD.
Features
- POSIX Compliance - Works with existing shell scripts and POSIX-standard features
- Interactive Editing - Full line editing with cursor navigation, history, and tab completion
- Customizable Prompt - Git integration, colors, and flexible PS1 escape sequences
- Scripting Support - Variables, control structures, functions, and command substitution
- Modern Conveniences - Syntax highlighting, autosuggestions, and configurable colors
Quick Start
# Install on macOS
brew install juliojimenez/hash/hash-shell
# Install on Debian/Ubuntu
echo "deb [trusted=yes] https://hash-shell.org/apt/ stable main" | sudo tee /etc/apt/sources.list.d/hash-shell.list
sudo apt update && sudo apt install hash-shell
# Run hash
hash-shell
Command Line Options
| Option | Description |
|---|---|
-c string | Execute commands from string |
-i | Force interactive mode |
-l, --login | Run as a login shell |
-s | Read commands from standard input |
-v, --version | Print version information |
-h, --help | Show help message |
Example Session
$ hash-shell
hash v34
Type 'exit' to quit
#> echo "Hello, World!"
Hello, World!
#> for i in 1 2 3; do echo "Number: $i"; done
Number: 1
Number: 2
Number: 3
#> exit
Getting Help
- Documentation: You’re reading it!
- Issues: GitHub Issues
- Discussions: GitHub Discussions
License
Hash is open source software licensed under the Apache 2.0 License.
Continue to Installation to get started.
Installation Overview
Hash can be installed on Linux, macOS, and FreeBSD through various methods.
Quick Install
| Platform | Method | Command |
|---|---|---|
| macOS | Homebrew | brew install juliojimenez/hash/hash-shell |
| Debian/Ubuntu | APT | See APT instructions |
| All platforms | From source | make && sudo make install |
| Docker | Pull image | docker pull juliojimenez/hash-shell |
Choose Your Installation Method
- Linux - Binary releases for x86_64 and ARM64
- macOS - Homebrew or binary release
- FreeBSD - Binary release or ports
- APT Repository - Debian/Ubuntu with automatic updates
- Docker - Containerized hash shell
- From Source - Build from source code
System Requirements
- 64-bit operating system (x86_64 or ARM64)
- Linux kernel 3.2+ / macOS 10.15+ / FreeBSD 12+
- libc (glibc on Linux, system libc on macOS/BSD)
- Terminal with ANSI color support (optional, for colored output)
Verify Installation
After installation, verify hash is working:
hash-shell --version
# hash v34
hash-shell
# hash v34
# Type 'exit' to quit
#>
Next Steps
After installing, see Getting Started to configure your shell.
Linux Installation
Binary Release
Linux x86_64
curl -LO https://github.com/juliojimenez/hash/releases/download/v34/hash-shell-v34-linux-x86_64
chmod +x hash-shell-v34-linux-x86_64
sudo mv hash-shell-v34-linux-x86_64 /usr/local/bin/hash-shell
hash-shell
Linux ARM64
curl -LO https://github.com/juliojimenez/hash/releases/download/v34/hash-shell-v34-linux-aarch64
chmod +x hash-shell-v34-linux-aarch64
sudo mv hash-shell-v34-linux-aarch64 /usr/local/bin/hash-shell
hash-shell
Debian/Ubuntu Packages
Ubuntu 24.04 (Noble)
curl -LO https://github.com/juliojimenez/hash/releases/download/v34/hash-shell_34-1~noble_amd64.deb
sudo dpkg -i hash-shell_34-1~noble_amd64.deb
Ubuntu 22.04 (Jammy)
curl -LO https://github.com/juliojimenez/hash/releases/download/v34/hash-shell_34-1~jammy_amd64.deb
sudo dpkg -i hash-shell_34-1~jammy_amd64.deb
Debian Bookworm
curl -LO https://github.com/juliojimenez/hash/releases/download/v34/hash-shell_34-1~bookworm_amd64.deb
sudo dpkg -i hash-shell_34-1~bookworm_amd64.deb
Debian Trixie
curl -LO https://github.com/juliojimenez/hash/releases/download/v34/hash-shell_34-1~trixie_amd64.deb
sudo dpkg -i hash-shell_34-1~trixie_amd64.deb
APT Repository (Recommended)
For automatic updates, use the APT repository:
echo "deb [trusted=yes] https://hash-shell.org/apt/ stable main" | sudo tee /etc/apt/sources.list.d/hash-shell.list
sudo apt update
sudo apt install hash-shell
See APT Installation for more details.
Verify Installation
hash-shell --version
hash-shell
macOS Installation
Homebrew (Recommended)
The easiest way to install hash on macOS:
brew install juliojimenez/hash/hash-shell
hash-shell
Upgrade
brew upgrade hash-shell
Uninstall
brew uninstall hash-shell
Binary Release
Apple Silicon (M1/M2/M3)
curl -LO https://github.com/juliojimenez/hash/releases/download/v34/hash-shell-v34-darwin-arm64
chmod +x hash-shell-v34-darwin-arm64
sudo mv hash-shell-v34-darwin-arm64 /usr/local/bin/hash-shell
hash-shell
From Source
See Building from Source for compilation instructions.
Setting as Default Shell
Homebrew Installation
sudo bash -c 'echo "$(brew --prefix)/bin/hash-shell" >> /etc/shells'
chsh -s "$(brew --prefix)/bin/hash-shell"
Binary Installation
sudo bash -c 'echo "/usr/local/bin/hash-shell" >> /etc/shells'
chsh -s /usr/local/bin/hash-shell
Log out and log back in for changes to take effect.
Verify Installation
hash-shell --version
hash-shell
FreeBSD Installation
Binary Release
FreeBSD x86_64
fetch https://github.com/juliojimenez/hash/releases/download/v34/hash-shell-v34-freebsd-x86_64
chmod +x hash-shell-v34-freebsd-x86_64
sudo mv hash-shell-v34-freebsd-x86_64 /usr/local/bin/hash-shell
hash-shell
FreeBSD ARM64
fetch https://github.com/juliojimenez/hash/releases/download/v34/hash-shell-v34-freebsd-aarch64
chmod +x hash-shell-v34-freebsd-aarch64
sudo mv hash-shell-v34-freebsd-aarch64 /usr/local/bin/hash-shell
hash-shell
From Source
See Building from Source for compilation instructions.
Setting as Default Shell
sudo sh -c 'echo "/usr/local/bin/hash-shell" >> /etc/shells'
chsh -s /usr/local/bin/hash-shell
Log out and log back in for changes to take effect.
Verify Installation
hash-shell --version
hash-shell
APT Repository Installation
Hash shell can be installed on Debian and Ubuntu systems using APT with automatic updates.
URL Change Notice: The APT repository has moved from
juliojimenez.github.io/hash/tohash-shell.org/apt/. If you previously configured the old URL, update your sources list:sudo sed -i 's|juliojimenez.github.io/hash/|hash-shell.org/apt/|' /etc/apt/sources.list.d/hash-shell.list sudo apt update
Quick Install
# Add the repository
echo "deb [trusted=yes] https://hash-shell.org/apt/ stable main" | sudo tee /etc/apt/sources.list.d/hash-shell.list
# Update and install
sudo apt update
sudo apt install hash-shell
Upgrade
Once installed via the repository, upgrade like any other package:
sudo apt update
sudo apt upgrade hash-shell
Or upgrade all packages:
sudo apt update
sudo apt upgrade
Supported Distributions
| Distribution | Codename | Architecture |
|---|---|---|
| Ubuntu 24.04 | noble | amd64 |
| Ubuntu 22.04 | jammy | amd64 |
| Debian 12 | bookworm | amd64 |
| Debian 13 | trixie | amd64 |
Direct Download (Alternative)
If you prefer not to use the repository, download packages directly:
Ubuntu 24.04 (Noble)
curl -LO https://github.com/juliojimenez/hash/releases/latest/download/hash-shell_*~noble_amd64.deb
sudo dpkg -i hash-shell_*~noble_amd64.deb
Ubuntu 22.04 (Jammy)
curl -LO https://github.com/juliojimenez/hash/releases/latest/download/hash-shell_*~jammy_amd64.deb
sudo dpkg -i hash-shell_*~jammy_amd64.deb
Debian 12 (Bookworm)
curl -LO https://github.com/juliojimenez/hash/releases/latest/download/hash-shell_*~bookworm_amd64.deb
sudo dpkg -i hash-shell_*~bookworm_amd64.deb
Note: Direct downloads won’t receive automatic updates.
Verify Installation
hash-shell
# hash v34
# Type 'exit' to quit
Set as Default Shell
echo "/usr/bin/hash-shell" | sudo tee -a /etc/shells
chsh -s /usr/bin/hash-shell
Log out and log back in for changes to take effect.
Uninstall
sudo apt remove hash-shell
sudo rm /etc/apt/sources.list.d/hash-shell.list
sudo apt update
Troubleshooting
“Repository does not have a Release file”
Make sure you’re using the correct URL:
cat /etc/apt/sources.list.d/hash-shell.list
# Should contain:
# deb [trusted=yes] https://hash-shell.org/apt/ stable main
Package Not Found After Adding Repository
sudo apt update
Dependency Issues
sudo apt --fix-broken install
GPG Key Warning
The repository uses [trusted=yes] for simplicity. This is safe for personal use but means apt won’t verify package signatures.
Docker Installation
Hash shell is available as a Docker image for containerized usage.
Quick Start
# Pull the image
docker pull juliojimenez/hash-shell
# Run interactive shell
docker run -it juliojimenez/hash-shell
Usage
Interactive Shell
docker run -it juliojimenez/hash-shell
Run a Script
Mount your scripts directory and execute:
docker run -v $(pwd):/scripts juliojimenez/hash-shell /scripts/myscript.sh
Run a Command
docker run juliojimenez/hash-shell -c 'echo "Hello from hash!"'
Building Locally
You can build the Docker image from source:
git clone https://github.com/juliojimenez/hash.git
cd hash
docker build -t hash-shell .
docker run -it hash-shell
Docker Compose
Example docker-compose.yml:
version: '3'
services:
shell:
image: juliojimenez/hash-shell
stdin_open: true
tty: true
volumes:
- ./scripts:/scripts
Run with:
docker-compose run shell
Use Cases
- Testing: Run hash in an isolated environment
- CI/CD: Execute shell scripts in containers
- Development: Test scripts across different environments
- Learning: Experiment without installing locally
Building from Source
Build hash shell from source code on any supported platform.
Prerequisites
- GCC or Clang compiler
- Make build system
- Git (for cloning)
Debian/Ubuntu
sudo apt install build-essential git
macOS
Install Xcode Command Line Tools:
xcode-select --install
FreeBSD
pkg install git gmake
Build Steps
# Clone the repository
git clone https://github.com/juliojimenez/hash.git
cd hash
# Build
make
# Install (to /usr/local/bin)
sudo make install
Build Options
Debug Build
Build with debug symbols:
make debug
Clean Build
Remove build artifacts:
make clean
make
Custom Installation Path
sudo make install PREFIX=/opt/hash
Running Tests
# Setup test framework (first time only)
make test-setup
# Run unit tests
make test
# Run integration tests
./test.sh
Verify Installation
hash-shell --version
hash-shell
Uninstall
sudo make uninstall
Development
For development setup, see the Contributing Guide.
Quick Start
Welcome to hash shell! This guide will help you get started with the basics.
First Steps
After installing hash, start it by running:
hash-shell
You’ll see:
hash v34
Type 'exit' to quit
#>
Basic Commands
Hash works like any POSIX shell:
#> echo "Hello, World!"
Hello, World!
#> ls -la
# Lists files
#> cd ~/projects
# Changes directory
#> pwd
/home/user/projects
Command Line Options
| Option | Description |
|---|---|
-c string | Execute commands from string |
-i | Force interactive mode |
-l, --login | Run as a login shell |
-s | Read commands from standard input |
-v, --version | Print version information |
-h, --help | Show help message |
Examples
# Execute a command
hash-shell -c 'echo "Hello!"'
# Run a script
hash-shell script.sh
# Run as login shell
hash-shell --login
Configuration
Hash loads configuration from ~/.hashrc on startup. Create your configuration file:
cp /path/to/hash/examples/.hashrc ~/.hashrc
Or create a minimal one:
# ~/.hashrc
alias ll='ls -lah'
alias gs='git status'
set PS1='\W\g \e#>\e '
See Configuration (.hashrc) for complete details.
Key Features
- Line Editing: Arrow keys, Ctrl+A/E, history navigation
- Tab Completion: Complete commands, files, and directories
- Git Integration: Branch and status in prompt
- Aliases: Create command shortcuts
- Scripting: POSIX-compatible shell scripts
Next Steps
- Configure your .hashrc - Customize your shell
- Set up as login shell - Make hash your default
- Learn line editing - Master keyboard shortcuts
- Write scripts - Automate tasks
Configuration (.hashrc)
Hash shell supports configuration through a .hashrc file in your home directory.
Quick Start
Create your configuration file:
vim ~/.hashrc
Example .hashrc:
# Aliases
alias ll='ls -lah'
alias gs='git status'
alias gp='git push'
# Environment
export EDITOR=vim
export PATH=$HOME/bin:$PATH
# Prompt
set PS1='\W\g \e#>\e '
# Options
set colors=on
set welcome=off
Reload after editing:
source ~/.hashrc
Aliases
Define command shortcuts:
alias name='command'
Examples
# Navigation
alias ..='cd ..'
alias ...='cd ../..'
# Safety
alias rm='rm -i'
alias cp='cp -i'
alias mv='mv -i'
# Git
alias gs='git status'
alias ga='git add'
alias gc='git commit'
alias gp='git push'
alias gd='git diff'
# Docker
alias d='docker'
alias dc='docker-compose'
alias dps='docker ps'
Managing Aliases
# List all aliases
alias
# Show specific alias
alias ll
# Remove alias
unalias ll
Environment Variables
Set variables with export:
export VARIABLE=value
export VARIABLE='value with spaces'
export VARIABLE="value with $substitution"
Common Variables
# Editor
export EDITOR=vim
export VISUAL=vim
# Path
export PATH=$HOME/bin:$PATH
# Locale
export LANG=en_US.UTF-8
# Custom
export MY_PROJECT=~/projects/myapp
Shell Options
Configure behavior with set:
set option=value
Available Options
| Option | Values | Description |
|---|---|---|
colors | on/off | Enable colored output |
welcome | on/off | Show welcome message |
PS1 | format | Custom prompt |
set colors=on
set welcome=off
set PS1='\W\g \e#>\e '
Prompt Customization
Set your prompt with the PS1 variable:
# Current directory + git + exit color
set PS1='\W\g \e#>\e '
# Full path + git
set PS1='\w\g \e#>\e '
# User@host:path
set PS1='\u@\h:\w\$ '
Escape Sequences
| Sequence | Description |
|---|---|
\w | Full current path |
\W | Current directory name |
\u | Username |
\h | Hostname |
\g | Git branch with status |
\e | Exit code color |
\$ | $ for user, # for root |
\n | Newline |
See Prompt Customization for complete details.
Comments
Lines starting with # are ignored:
# This is a comment
alias ll='ls -lah' # Inline comment
Tips
Organize by Category
# ============================================================================
# NAVIGATION
# ============================================================================
alias ..='cd ..'
alias projects='cd ~/projects'
# ============================================================================
# GIT
# ============================================================================
alias gs='git status'
alias gp='git push'
Test Before Adding
Try aliases interactively first:
#> alias test='echo "works"'
#> test
works
# Now add to .hashrc
Reload After Changes
source ~/.hashrc
Troubleshooting
Alias Not Working
Check syntax (quotes required):
# Wrong
alias ll=ls -lah
# Correct
alias ll='ls -lah'
Variable Not Set
Use export:
# Wrong
MY_VAR=value
# Correct
export MY_VAR=value
Config Not Loading
Check file location and reload:
ls -la ~/.hashrc
source ~/.hashrc
Login Shell Setup
Hash supports proper login shell behavior, loading different startup files depending on how it’s invoked.
Setting Hash as Default Shell
Linux (chsh)
sudo bash -c 'echo "/usr/local/bin/hash-shell" >> /etc/shells'
chsh -s /usr/local/bin/hash-shell
Linux (usermod)
sudo usermod -s /usr/local/bin/hash-shell your_username
macOS
sudo bash -c 'echo "/usr/local/bin/hash-shell" >> /etc/shells'
chsh -s /usr/local/bin/hash-shell
If installed with Homebrew:
sudo bash -c 'echo "$(brew --prefix)/bin/hash-shell" >> /etc/shells'
chsh -s "$(brew --prefix)/bin/hash-shell"
FreeBSD
sudo sh -c 'echo "/usr/local/bin/hash-shell" >> /etc/shells'
chsh -s /usr/local/bin/hash-shell
Note: Log out and log back in for changes to take effect.
Login Shell Detection
Hash considers itself a login shell when:
argv[0]starts with-(how SSH/login invoke shells)--loginor-lflag is used
Login shells display “(login)” in the welcome message.
Startup File Order
Login Shell
When invoked as a login shell:
/etc/profile- System-wide settings- User profile (first found):
~/.hash_profile~/.hash_login~/.profile
~/.hashrc- Interactive settings
Non-Login Interactive Shell
~/.hashrc only
Configuration Files
~/.hash_profile
User-specific login configuration:
# Environment
export EDITOR=vim
export PAGER=less
# PATH
export PATH=$HOME/bin:$HOME/.local/bin:$PATH
~/.hashrc
Interactive settings:
# Aliases
alias ll='ls -lah'
alias gs='git status'
# Prompt
set PS1='\W\g \e#>\e '
~/.hash_logout
Executed when exiting a login shell:
# Clear screen
clear
# Farewell message
echo "Goodbye! Session ended at $(date)"
# Cleanup
rm -rf ~/tmp/session_* 2>/dev/null
The logout Command
Use logout to exit login shells:
# In a login shell
#> logout
Bye :)
# In a non-login shell
#> logout
hash: logout: not a login shell
# Use 'exit' instead
Best Practices
Separate Concerns
- ~/.hash_profile: Environment variables, PATH
- ~/.hashrc: Aliases, prompt settings
- ~/.hash_logout: Cleanup tasks
Example Setup
~/.hash_profile:
export EDITOR=vim
export GOPATH=$HOME/go
export PATH=$HOME/bin:$GOPATH/bin:$PATH
~/.hashrc:
set PS1='\u@\h:\W\g #> '
alias ll='ls -lah'
set colors=on
~/.hash_logout:
clear
echo "Session ended. Goodbye!"
Testing
# Test as login shell
hash-shell --login
# Check for "(login)" in welcome message
hash v34 (login)
Type 'exit' to quit
Interactive Features
Hash shell includes powerful interactive features for efficient command-line use.
Features Overview
| Feature | Description |
|---|---|
| Line Editing | Cursor navigation, editing shortcuts |
| Tab Completion | Complete commands, files, directories |
| Command History | Navigate and search previous commands |
| Prompt Customization | Git integration, colors, custom format |
Quick Reference
Line Editing
| Key | Action |
|---|---|
Ctrl+A | Move to beginning of line |
Ctrl+E | Move to end of line |
Ctrl+U | Delete to beginning |
Ctrl+K | Delete to end |
Ctrl+W | Delete word backward |
Ctrl+L | Clear screen |
Ctrl+C | Cancel current line |
History Navigation
| Key | Action |
|---|---|
Up | Previous command |
Down | Next command |
!! | Repeat last command |
!n | Run command number n |
!prefix | Run last command starting with prefix |
Tab Completion
| Context | Completes |
|---|---|
| First word | Commands, aliases, builtins |
| Other words | Files, directories |
| Double TAB | Show all matches |
Default Prompt
The default prompt shows:
/home/user/projects git:(main) #>
- Blue path
- Green/Yellow git status (clean/dirty)
- Cyan branch name
- Blue/Red
#>based on exit code
Configuration
Configure interactive features in ~/.hashrc:
# Custom prompt
set PS1='\W\g \e#>\e '
# Enable colors
set colors=on
Line Editing
Hash shell includes a custom line editor with full cursor navigation and editing capabilities.
Key Bindings
Cursor Movement
| Key | Action |
|---|---|
Left Arrow | Move cursor left |
Right Arrow | Move cursor right |
Up Arrow | Previous command in history |
Down Arrow | Next command in history |
Ctrl+A | Move to beginning of line |
Ctrl+E | Move to end of line |
Home | Move to beginning of line |
End | Move to end of line |
Editing
| Key | Action |
|---|---|
Backspace | Delete character before cursor |
Delete | Delete character at cursor |
Ctrl+H | Same as Backspace |
Ctrl+U | Delete from cursor to beginning |
Ctrl+K | Delete from cursor to end |
Ctrl+W | Delete word backward |
Other
| Key | Action |
|---|---|
Tab | Auto-complete |
Ctrl+L | Clear screen |
Ctrl+C | Cancel current line |
Ctrl+D | EOF (exit if line empty) |
Enter | Submit command |
Features
Insert Mode
Hash uses insert mode (like bash):
- Typing inserts characters at cursor position
- Existing text shifts right
- Edit anywhere in the line
hello world
^
cursor
Type 'beautiful ' → hello beautiful world
Tab Completion
Press TAB to auto-complete:
Single match:
#> ec<TAB>
#> echo # Completed with space
Multiple matches - first TAB:
#> cat te<TAB>
#> cat test # Common prefix
Multiple matches - second TAB:
test.txt test.md testing.log
#> cat test
See Tab Completion for details.
Ctrl+C Handling
Cancel current input and get a fresh prompt:
#> some long command^C
#>
Ctrl+D Behavior
- On empty line: Exits the shell
- On non-empty line: Does nothing
Tips & Tricks
Quick Edits
# Typed wrong command
#> cat file.tx
# Press Ctrl+A, Ctrl+K, type correct command
Delete Mistakes
#> echo hello worldddd
# Press Ctrl+W to delete "worldddd"
#> echo hello
Clear and Restart
#> some complex command
# Press Ctrl+U to clear
#>
Cancel Without Running
#> rm -rf /important
# Press Ctrl+C
^C
#>
Troubleshooting
Keys Not Working
Check terminal type:
echo $TERM
# Should be xterm, xterm-256color, etc.
Arrow Keys Print Escape Sequences
Set proper terminal:
export TERM=xterm-256color
Comparison with Bash
Supported (Same as Bash)
- Arrow key navigation
- History navigation
- Ctrl+A, Ctrl+E
- Ctrl+U, Ctrl+K, Ctrl+W
- Ctrl+L, Ctrl+C, Ctrl+D
- Home/End keys
- Insert mode
Not Yet Supported
- Ctrl+R (reverse search)
- Ctrl+T (transpose)
- Alt key bindings
- Vi mode
- Custom key bindings
Tab Completion
Hash shell includes intelligent tab completion for commands, files, and directories.
Basic Usage
Command Completion
Press TAB after typing part of a command:
#> ec<TAB>
#> echo # Completed!
#> gi<TAB>
#> git # Completed from PATH
File/Directory Completion
#> cat test<TAB>
#> cat testfile.txt # Completed!
#> cd Doc<TAB>
#> cd Documents/ # Directory with trailing /
Multiple Matches
First TAB - Complete common prefix:
#> cat test<TAB>
#> cat test # Common prefix
Second TAB - Show all matches:
#> cat test<TAB><TAB>
testfile.txt test.md test_data.csv
#> cat test
What Gets Completed
First Word (Command)
Completes from:
- Built-in commands (cd, exit, alias, etc.)
- Aliases from .hashrc
- Executables in PATH
#> g<TAB><TAB>
ga gc gd git gl gp gs
Subsequent Words (Files/Directories)
Completes:
- Files and directories
- Paths with tilde expansion
- Relative and absolute paths
#> cat /<TAB>
bin/ boot/ dev/ etc/ home/ ...
#> cd ~/.c<TAB>
#> cd ~/.config/
Features
Directory Handling
Directories get trailing /:
#> cd Doc<TAB>
#> cd Documents/
Tilde Expansion
#> cat ~/.<TAB>
~/.bashrc ~/.config/ ~/.hashrc ...
Path Completion
#> ls ~/projects/hash/src/m<TAB>
#> ls ~/projects/hash/src/main.c
Case Sensitivity
Completion is case-sensitive:
#> cat Test<TAB> # Won't match "test.txt"
#> cat test<TAB> # Matches "test.txt"
Behavior
Single Match
- Completes full name
- Adds space after (ready for next arg)
- Directories get
/
Multiple Matches
First TAB: Complete common prefix Second TAB: Show all matches in columns
No Matches
Bell/beep sound, line unchanged.
After Operators
Completion works after command chaining:
#> ls && ca<TAB>
#> ls && cat
#> cd /tmp ; pw<TAB>
#> cd /tmp ; pwd
Tips & Tricks
Quick Directory Navigation
#> cd /u/l/b<TAB>
#> cd /usr/local/bin/
Exploring Directories
#> ls /etc/<TAB><TAB>
# Shows all files in /etc/
Command Discovery
#> git<TAB><TAB>
git git-shell gitk ...
Limitations
Current limitations (may be added later):
- No variable name completion (after
$) - No username completion (after
~) - No command-specific completion (git subcommands)
- No programmable completion
Comparison with Bash
| Feature | Hash | Bash |
|---|---|---|
| Command completion | Yes | Yes |
| File/directory completion | Yes | Yes |
| Alias completion | Yes | Yes |
| Path completion | Yes | Yes |
| Double-TAB shows matches | Yes | Yes |
| Variable completion | No | Yes |
| Programmable completion | No | Yes |
Command History
Hash shell maintains persistent command history with bash compatibility.
Basic Usage
Up/Down Arrows
Navigate through history:
#> echo first
#> echo second
#> echo third
# Press Up arrow
#> echo third # Most recent
# Press Up again
#> echo second
History Command
List all commands:
#> history
0 ls -la
1 cd /tmp
2 echo hello
3 git status
History Expansion
!! - Last Command
#> echo hello
hello
#> !!
echo hello
hello
#> apt update
Permission denied
#> sudo !!
sudo apt update
!n - Command by Number
#> history
0 ls -la
1 cd /tmp
2 echo test
#> !1
cd /tmp
!-n - Relative History
#> echo first
#> echo second
#> echo third
#> !-2
echo second
second
!prefix - Last Command Starting With
#> git status
#> ls -la
#> pwd
#> !git
git status
Environment Variables
HISTSIZE
Commands kept in memory:
export HISTSIZE=1000 # Default
export HISTSIZE=5000 # More
export HISTSIZE=-1 # Unlimited
HISTFILESIZE
Commands saved to file:
export HISTFILESIZE=2000 # Default
export HISTFILESIZE=-1 # Unlimited
HISTFILE
History file location:
# Default
~/.hash_history
# Custom
export HISTFILE=~/my_history
HISTCONTROL
Control what gets saved:
export HISTCONTROL=ignorespace # Skip space-prefixed
export HISTCONTROL=ignoredups # Skip consecutive dups
export HISTCONTROL=ignoreboth # Both (recommended)
export HISTCONTROL=erasedups # Remove all dups
Privacy Feature
Commands starting with space are NOT saved:
#> echo public
# Saved to history
#> echo private
# NOT saved (leading space)
What Gets Saved
- Successfully executed commands
- Commands with errors
- Multi-line commands (&&, ||, ;)
What Doesn’t Get Saved
- Empty lines
- Whitespace-only lines
- Duplicate of previous command
- Commands starting with space
History File
Location
Default: ~/.hash_history
Incremental Saving
History saves immediately after each command:
#> echo test1
# Immediately saved
#> echo test2
# Also saved
# Even if shell crashes, both are preserved!
Manual Commands
history -w # Force save
history -r # Reload from file
history -c # Clear history (memory only)
Examples
Repeat Last Command
#> make
#> !!
make
Repeat with Sudo
#> apt update
Permission denied
#> sudo !!
sudo apt update
History Search
#> history | grep git
# See all git commands
Private Commands
#> export API_KEY=secret
# Leading space - not saved!
Troubleshooting
History Not Saving
Check file permissions:
ls -la ~/.hash_history
!! Not Working
Make sure you’ve run a command first.
Up Arrow Not Working
Check terminal:
echo $TERM
Comparison with Bash
| Feature | Hash | Bash |
|---|---|---|
!! | Yes | Yes |
!n | Yes | Yes |
!-n | Yes | Yes |
!prefix | Yes | Yes |
| Up/Down arrows | Yes | Yes |
| History file | Yes | Yes |
| Privacy (space prefix) | Yes | Yes |
Ctrl+R | No | Yes |
!?pattern | No | Yes |
^old^new | No | Yes |
Prompt Customization
Hash shell supports powerful prompt customization through the PS1 variable.
Default Prompt
/home/user/projects git:(main) #>
- Blue current path
- Green
git:(yellow if dirty) - Cyan branch name
- Blue
#>(red if last command failed)
Quick Start
In ~/.hashrc:
# Simple prompt
set PS1='\W #> '
# Full path with git
set PS1='\w\g \e#>\e '
# User@host:path
set PS1='\u@\h:\w\$ '
Or as environment variable:
export PS1='\w\g \e#>\e '
Escape Sequences
Path Information
| Sequence | Description | Example |
|---|---|---|
\w | Full current path | /home/user/projects |
\W | Current directory only | projects |
User Information
| Sequence | Description |
|---|---|
\u | Username |
\h | Hostname |
\$ | $ for user, # for root |
Git Integration
| Sequence | Description |
|---|---|
\g | Git branch with status |
The \g sequence:
- Shows nothing outside git repos
- Shows
git:(branch)in repos git:is green if clean, yellow if dirty- Branch name is cyan
Exit Code Indicator
| Sequence | Description |
|---|---|
\e | Sets color based on exit code |
Use \e before and after text:
set PS1='\w \e#>\e '
# ^ ^
# | Reset color
# Set color (blue=success, red=fail)
Special Characters
| Sequence | Description |
|---|---|
\n | Newline |
\\ | Literal backslash |
Example Prompts
Minimal
set PS1='\W #> '
# Output: projects #>
Classic
set PS1='\u@\h:\w \$ '
# Output: user@hostname:/home/user $
Full Path with Git
set PS1='\w\g \e#>\e '
# Output: /home/user/projects git:(main) #>
Two-Line
set PS1='\w\g\n\e#>\e '
# Output:
# /home/user/projects git:(main)
# #>
Powerline Style
set PS1='\u@\h \w\g\n\e\e '
Color Behavior
Path Colors
\w and \W are bold blue
Git Colors
git:is green when cleangit:is yellow when dirty- Branch name is cyan
Exit Code Colors
\esets blue on success (exit 0)\esets red on failure (exit non-zero)
Tips
Test Before Setting
#> export PS1='\W\g \e#>\e '
projects git:(main) #>
Keep It Simple
Complex prompts can slow down the shell:
# Fast
set PS1='\W #> '
# Slower (git checks)
set PS1='\w\g \e#>\e '
Two-Line for Long Paths
set PS1='\w\g\n\e#>\e '
Disable Git in Large Repos
set PS1='\w \e#>\e ' # No \g
Troubleshooting
Colors Not Showing
Enable colors:
set colors=on
Check terminal:
echo $TERM
# Should be xterm-256color or similar
Git Branch Not Showing
Verify you’re in a git repo:
git status
Ensure \g is in your PS1.
Exit Colors Not Working
Wrap characters between two \e:
# Wrong
set PS1='\w \e#> '
# Correct
set PS1='\w \e#>\e '
Comparison with Bash
Similar
\w,\W,\u,\h,\$work the same- Color support
- Environment variable support
Hash-Specific
\g- Git integration\e- Exit code color- Automatic
~substitution
Not Supported (Yet)
\d- Date\t- Time\!- History number\[and\]- Non-printing sequences
Shell Scripting
Hash shell supports POSIX-compatible shell scripting for automation and reusable scripts.
Running Scripts
Shebang Line
Create a script with the hash shebang:
#!/usr/local/bin/hash-shell
echo "Hello from hash!"
Make executable and run:
chmod +x script.sh
./script.sh
Direct Execution
hash-shell script.sh
Command String
hash-shell -c 'echo "Hello"; echo "World"'
From Standard Input
echo 'echo "Hello"' | hash-shell -s
Script Arguments
Positional Parameters
#!/usr/local/bin/hash-shell
echo "Script name: $0"
echo "First arg: $1"
echo "Second arg: $2"
echo "All args: $@"
echo "Arg count: $#"
Run:
./script.sh hello world
# Output:
# Script name: ./script.sh
# First arg: hello
# Second arg: world
# All args: hello world
# Arg count: 2
Special Variables
| Variable | Description |
|---|---|
$0 | Script name |
$1-$9 | Positional arguments |
$# | Number of arguments |
$@ | All arguments (separate words) |
$* | All arguments (single word) |
$? | Exit code of last command |
$$ | Process ID of shell |
Functions
greet() {
echo "Hello, $1!"
}
greet "World"
With return value:
is_even() {
if [ $(($1 % 2)) -eq 0 ]; then
return 0 # true
else
return 1 # false
fi
}
if is_even 4; then
echo "4 is even"
fi
Variables
Assignment
NAME="value" # No spaces around =
export NAME="value" # Export to environment
Expansion
echo $NAME # Simple
echo ${NAME} # Braced
echo "${NAME}" # Quoted (preserves whitespace)
Exit Codes
#!/usr/local/bin/hash-shell
if [ ! -f "$1" ]; then
echo "Error: File not found" >&2
exit 1
fi
# Process file...
exit 0
Common codes:
0- Success1- General error2- Misuse of command
Comments
# This is a comment
echo "Hello" # Inline comment
Best Practices
- Always quote variables:
"$var"prevents word splitting - Use meaningful names:
FILE_COUNTnotfc - Check for errors: Test return codes
- Add comments: Explain complex logic
- Use exit codes: Return 0 for success
- Validate input: Check arguments first
Practical Example
#!/usr/local/bin/hash-shell
# Backup script
BACKUP_DIR="/backup"
DATE=$(date +%Y%m%d)
if [ ! -d "$BACKUP_DIR" ]; then
mkdir -p "$BACKUP_DIR"
fi
for dir in /home/*; do
if [ -d "$dir" ]; then
user=$(basename "$dir")
tar -czf "$BACKUP_DIR/${user}_${DATE}.tar.gz" "$dir"
echo "Backed up: $user"
fi
done
echo "Backup complete!"
See Also
Variables & Expansion
Hash shell supports environment variable expansion in commands and arguments.
Basic Syntax
$VAR - Simple Expansion
#> export NAME=Julio
#> echo Hello $NAME
Hello Julio
${VAR} - Braced Expansion
Use braces when concatenating:
#> export PRE=test
#> echo ${PRE}ing
testing
#> echo $PREing
# Tries to expand $PREing (undefined)
Special Variables
$? - Exit Code
#> true
#> echo $?
0
#> false
#> echo $?
1
$$ - Process ID
#> echo $$
12345
$0 - Shell Name
#> echo $0
hash
Variable Name Rules
Valid names:
- Start with letter or underscore
- Contain letters, numbers, underscores
- Case-sensitive
$HOME # Valid
$MY_VAR # Valid
$_private # Valid
$123 # Invalid (starts with number)
$my-var # Invalid (hyphen)
Escaping
Prevent expansion with backslash:
#> echo \$HOME
$HOME
#> echo Price: \$5
Price: $5
Quote Handling
Double Quotes - Variables Expand
#> export NAME=Julio
#> echo "Hello $NAME"
Hello Julio
Single Quotes - No Expansion
#> echo 'Hello $NAME'
Hello $NAME
Common Patterns
Building Paths
#> export PROJECT=myapp
#> export BUILD_DIR=/tmp/builds
#> mkdir -p ${BUILD_DIR}/${PROJECT}
PATH Manipulation
#> export PATH=$HOME/bin:$PATH
Configuration
# In .hashrc
export EDITOR=vim
export PROJECT_DIR=~/projects
alias edit='$EDITOR'
alias proj='cd $PROJECT_DIR'
Undefined Variables
Expand to empty string:
#> unset UNDEFINED
#> echo "Value: $UNDEFINED"
Value:
Limitations
Current limitations (may be added):
${VAR:-default}- Default value${VAR:=value}- Assign default${#VAR}- Length${VAR%pattern}- Remove suffix${VAR#pattern}- Remove prefix
Control Structures
Hash shell supports POSIX-compatible control structures for flow control.
If/Then/Else
Syntax
if command; then
commands
fi
if command; then
commands
else
commands
fi
if command; then
commands
elif command; then
commands
else
commands
fi
Examples
# Simple if
if [ -f "/etc/passwd" ]; then
echo "passwd file exists"
fi
# If/else
if [ "$USER" = "root" ]; then
echo "Running as root"
else
echo "Running as $USER"
fi
# If/elif/else
if [ $count -lt 0 ]; then
echo "Negative"
elif [ $count -eq 0 ]; then
echo "Zero"
else
echo "Positive"
fi
Any Command as Condition
if grep -q "pattern" file.txt; then
echo "Found"
fi
if command -v git > /dev/null; then
echo "Git installed"
fi
For Loops
Syntax
for var in list; do
commands
done
Examples
# List of words
for name in Alice Bob Charlie; do
echo "Hello, $name"
done
# Files
for file in *.txt; do
echo "Processing: $file"
done
# Arguments
for arg in "$@"; do
echo "Argument: $arg"
done
# Numbers
for i in $(seq 1 5); do
echo "Number: $i"
done
While Loops
Syntax
while command; do
commands
done
Examples
count=0
while [ $count -lt 5 ]; do
echo "Count: $count"
count=$((count + 1))
done
# Read lines
while read line; do
echo "Line: $line"
done < file.txt
# Infinite loop
while true; do
echo "Press Ctrl+C"
sleep 1
done
Until Loops
Syntax
until command; do
commands
done
Example
count=0
until [ $count -ge 5 ]; do
echo "Count: $count"
count=$((count + 1))
done
Case Statements
Syntax
case word in
pattern1)
commands
;;
pattern2|pattern3)
commands
;;
*)
default
;;
esac
Examples
case "$1" in
start)
echo "Starting..."
;;
stop)
echo "Stopping..."
;;
*)
echo "Usage: $0 {start|stop}"
;;
esac
# Pattern matching
case "$filename" in
*.txt)
echo "Text file"
;;
*.sh)
echo "Shell script"
;;
*)
echo "Unknown"
;;
esac
Nesting
for dir in /home/*; do
if [ -d "$dir" ]; then
for file in "$dir"/*.txt; do
if [ -f "$file" ]; then
echo "Found: $file"
fi
done
fi
done
Common Mistakes
Missing Spaces
# Wrong
if [-f "$file"]; then
# Correct
if [ -f "$file" ]; then
Unquoted Variables
# Wrong
for file in $files; do
# Correct
for file in "$files"; do
= vs -eq
# String comparison
if [ "$str" = "value" ]; then
# Numeric comparison
if [ $num -eq 5 ]; then
Test Command
The test command (or [ ]) evaluates expressions for use in conditionals.
File Tests
| Test | Description |
|---|---|
-e file | File exists |
-f file | Is a regular file |
-d file | Is a directory |
-r file | Is readable |
-w file | Is writable |
-x file | Is executable |
-s file | Has size > 0 |
-L file | Is a symbolic link |
Examples
if [ -f "/etc/passwd" ]; then
echo "File exists"
fi
if [ -d "$dir" ]; then
echo "Is a directory"
fi
if [ -r "$file" ] && [ -w "$file" ]; then
echo "Readable and writable"
fi
String Tests
| Test | Description |
|---|---|
-z string | String is empty |
-n string | String is not empty |
s1 = s2 | Strings are equal |
s1 != s2 | Strings are not equal |
Examples
if [ -z "$var" ]; then
echo "Variable is empty"
fi
if [ "$USER" = "root" ]; then
echo "Running as root"
fi
if [ "$a" != "$b" ]; then
echo "Strings differ"
fi
Numeric Tests
| Test | Description |
|---|---|
n1 -eq n2 | Equal |
n1 -ne n2 | Not equal |
n1 -lt n2 | Less than |
n1 -le n2 | Less than or equal |
n1 -gt n2 | Greater than |
n1 -ge n2 | Greater than or equal |
Examples
if [ $count -eq 0 ]; then
echo "Count is zero"
fi
if [ $x -lt 10 ]; then
echo "x is less than 10"
fi
if [ $a -ge $b ]; then
echo "a >= b"
fi
Logical Operators
| Operator | Description |
|---|---|
! expr | NOT |
expr -a expr | AND |
expr -o expr | OR |
Examples
# NOT
if [ ! -f "$file" ]; then
echo "File not found"
fi
# AND
if [ -f "$file" -a -r "$file" ]; then
echo "File exists and is readable"
fi
# OR
if [ -z "$a" -o -z "$b" ]; then
echo "At least one is empty"
fi
Combining with && and ||
# Preferred over -a and -o
if [ -f "$file" ] && [ -r "$file" ]; then
echo "File exists and readable"
fi
if [ -z "$a" ] || [ -z "$b" ]; then
echo "At least one is empty"
fi
Common Patterns
Check File Exists
if [ -f "$config" ]; then
source "$config"
fi
Check Directory
if [ ! -d "$dir" ]; then
mkdir -p "$dir"
fi
Validate Arguments
if [ $# -eq 0 ]; then
echo "Usage: $0 file" >&2
exit 1
fi
Compare Strings
if [ "$answer" = "yes" ]; then
proceed
fi
Numeric Comparison
if [ $count -gt 100 ]; then
echo "Too many items"
fi
Important Notes
Spaces Required
# Wrong
if [-f "$file"]; then
# Correct
if [ -f "$file" ]; then
Quote Variables
# Wrong (breaks if file has spaces)
if [ -f $file ]; then
# Correct
if [ -f "$file" ]; then
= vs ==
In POSIX shell, use = for string comparison:
# POSIX compliant
if [ "$a" = "$b" ]; then
# Also works but less portable
if [ "$a" == "$b" ]; then
Operators & Redirection
Hash shell supports Unix operators for connecting and controlling commands.
Overview
| Operator | Type | Description |
|---|---|---|
| | Pipe | Connect stdout to stdin |
> | Redirect | Output to file (overwrite) |
>> | Redirect | Output to file (append) |
< | Redirect | Input from file |
2> | Redirect | Stderr to file |
&> | Redirect | Both stdout and stderr |
&& | Chain | Run if previous succeeded |
|| | Chain | Run if previous failed |
; | Chain | Run sequentially |
& | Background | Run in background |
Quick Examples
Pipes
ls | grep txt
cat file.txt | sort | uniq
ps aux | grep nginx | wc -l
Redirection
echo "Hello" > file.txt # Overwrite
echo "World" >> file.txt # Append
cat < input.txt # Input from file
command 2> errors.log # Stderr
command &> all.log # Both
Command Chaining
make && make install # Run if success
cd /nonexistent || echo "Failed" # Run if fail
echo "Start"; command; echo "End" # Sequential
Background
long_command & # Run in background
Learn More
- Pipes - Connect command output to input
- I/O Redirection - Control file input/output
- Command Chaining - Sequential and conditional execution
- Command Substitution - Use command output as arguments
- Background Processes - Run commands in background
Pipes
Connect the output of one command to the input of another.
Basic Usage
#> ls | grep txt
file1.txt
test.txt
#> cat file.txt | grep pattern | sort | uniq
How Pipes Work
command1 | command2 | command3
command1stdout →command2stdincommand2stdout →command3stdincommand3stdout → terminal
Each command runs in its own process.
Exit Code
Pipeline exit code is from the last command:
#> false | true
#> echo $?
0 # From 'true', not 'false'
Common Patterns
Filtering
ls -la | grep txt
ps aux | grep nginx
cat log.txt | grep error
Counting
ls | wc -l # Count files
cat file.txt | wc -l # Count lines
ps aux | grep python | wc -l # Count processes
Text Processing
cat names.txt | sort | uniq
ls -la | awk '{print $5, $9}'
echo hello | tr '[:lower:]' '[:upper:]'
Data Analysis
cat data.csv | cut -d, -f2 | sort | uniq -c | sort -rn | head -10
With Other Features
With Aliases
alias count='wc -l'
ls | count
With Variables
export PATTERN=error
cat log.txt | grep $PATTERN
With Chaining
ls | grep txt && echo "Found txt files"
Difference from ||
| Operator | Purpose |
|---|---|
| | Connect stdout to stdin |
|| | Execute if previous failed |
echo hello | cat # Pipe: passes "hello" to cat
false || echo hello # OR: runs echo because false failed
Troubleshooting
Broken Pipe
If first command produces lots of output but second exits early:
cat huge_file.txt | head -10
# Normal behavior, handled automatically
No Output / Hanging
Command waiting for input:
cat | grep something
# Waiting forever - press Ctrl+C
Limitations
- No
|&(pipe stderr) - No process substitution
<(command)
I/O Redirection
Control where commands read from and write to.
Basic Redirections
Output (>)
Redirect stdout to file (overwrites):
echo "Hello" > output.txt
ls -la > filelist.txt
Append (>>)
Append to file:
echo "Line 1" > log.txt
echo "Line 2" >> log.txt
Input (<)
Read from file:
cat < input.txt
wc -l < file.txt
sort < unsorted.txt > sorted.txt
Error Redirection
Stderr (2>)
ls /nonexistent 2> errors.txt
command 2>> errors.log # Append
Stderr to Stdout (2>&1)
command 2>&1 | grep error
make 2>&1 | tee build.log
Both Streams (&>)
command &> all.log
# Same as: command > all.log 2>&1
Common Patterns
Discard Output
command > /dev/null # Discard stdout
command 2> /dev/null # Discard stderr
command &> /dev/null # Discard both
Logging
./script.sh &> script.log
./daily.sh &>> daily.log # Append
Separate Streams
command > output.txt 2> errors.txt
Processing Files
tr '[:lower:]' '[:upper:]' < input.txt > output.txt
sort < data.txt > sorted.txt
Order Matters
# Both to file
command > file 2>&1
# Both to pipe
command 2>&1 | grep error
File Descriptors
0= stdin1= stdout2= stderr
2> file # Redirect fd 2
1> file # Same as > file
0< file # Same as < file
Troubleshooting
Permission Denied
Check file/directory permissions.
Accidental Overwrite
Use >> to append, or check first:
test -f file.txt && echo "File exists!"
Limitations
- No heredoc (
<< EOF) - No here-string (
<<< "string") - No process substitution (
<(command)) - No noclobber protection
Command Chaining
Execute multiple commands with conditional or sequential logic.
Operators
| Operator | Name | Behavior |
|---|---|---|
&& | AND | Run next if previous succeeded (exit 0) |
|| | OR | Run next if previous failed (exit non-0) |
; | Sequential | Run next regardless |
AND (&&)
Run second command only if first succeeds:
make && make install
# make install runs only if make succeeds
cd /project && npm install
# npm install runs only if cd succeeds
mkdir -p dir && cd dir && touch file
# Creates dir, enters it, creates file
OR (||)
Run second command only if first fails:
cd /nonexistent || echo "Directory not found"
# echo runs because cd failed
command || exit 1
# Exit script if command fails
test -f config || cp default.conf config
# Copy default only if config doesn't exist
Sequential (;)
Run commands in sequence regardless of exit codes:
echo "Start"; command; echo "End"
# All three run
cd /tmp; ls; pwd
# All run even if one fails
Combined Patterns
Build and Install
make clean && make && make install
Create If Not Exists
test -d "$dir" || mkdir -p "$dir"
Try Multiple Options
command1 || command2 || command3
# Tries each until one succeeds
Fallback with Message
./run.sh || echo "Script failed" && exit 1
Grouped Commands
(cd /project && make) || echo "Build failed"
Exit Codes
AND Chain
true && echo "runs" # runs
false && echo "skipped" # skipped
OR Chain
true || echo "skipped" # skipped
false || echo "runs" # runs
Chain Exit Code
Exit code is from the last executed command:
true && false
echo $? # 1 (from false)
false || true
echo $? # 0 (from true)
Common Use Cases
Safe Directory Operations
cd /some/dir && rm -rf temp/
# Only deletes if cd succeeds
Conditional Execution
[ -f config.yml ] && source config.yml
Error Handling
command || { echo "Error" >&2; exit 1; }
Cleanup on Success
make && make test && make clean
With Other Features
With Pipes
cat file | grep pattern && echo "Found"
make 2>&1 | tee log && echo "Success"
With Redirection
command > output && echo "Saved"
With Variables
export DEBUG=1 && ./run.sh
Command Substitution
Use command output as part of another command.
Syntax
$(…) - Modern Syntax (Recommended)
result=$(command)
echo "Today is $(date)"
`…` - Backtick Syntax
result=`command`
echo "Today is `date`"
The $(...) syntax is preferred as it’s easier to nest and read.
Basic Usage
Capture Output
current_dir=$(pwd)
echo "You are in: $current_dir"
file_count=$(ls | wc -l)
echo "Files: $file_count"
In Commands
echo "Today is $(date)"
# Today is Mon Jan 1 12:00:00 UTC 2024
cd $(dirname "$0")
# Change to script's directory
In Variables
VERSION=$(cat VERSION)
HOSTNAME=$(hostname)
USER_HOME=$(eval echo ~$USER)
Common Patterns
Get Current Date/Time
NOW=$(date +%Y%m%d)
TIMESTAMP=$(date +%H%M%S)
Count Items
NUM_FILES=$(ls *.txt | wc -l)
NUM_LINES=$(wc -l < file.txt)
Find Commands
EDITOR=$(which vim)
GIT_ROOT=$(git rev-parse --show-toplevel)
Process Output
for file in $(find . -name "*.txt"); do
echo "Processing: $file"
done
for user in $(cat /etc/passwd | cut -d: -f1); do
echo "User: $user"
done
Nesting
The $(...) syntax allows easy nesting:
echo "Script dir: $(dirname $(realpath $0))"
# With backticks (harder to read)
echo "Script dir: `dirname \`realpath $0\``"
In Arithmetic
total=$(($(cat file1 | wc -l) + $(cat file2 | wc -l)))
With Quoting
# Preserve whitespace
files="$(ls -la)"
echo "$files"
# Word splitting (each line becomes argument)
for line in $(cat file.txt); do
echo "$line"
done
Exit Codes
Command substitution captures output, not exit code:
result=$(false)
echo $? # 1 (exit code available immediately after)
# Check both
if output=$(command); then
echo "Success: $output"
else
echo "Failed"
fi
Examples
Backup with Date
backup_file="backup_$(date +%Y%m%d).tar.gz"
tar -czf "$backup_file" /data
Dynamic Paths
cd "$(git rev-parse --show-toplevel)"
source "$(dirname $0)/config.sh"
Conditional on Output
if [ "$(whoami)" = "root" ]; then
echo "Running as root"
fi
Build Version String
VERSION="$(cat VERSION)-$(git rev-parse --short HEAD)"
Limitations
- Trailing newlines are stripped
- Very large output may be slow
- Exit code only available immediately after
Background Processes
Run commands in the background while continuing to use the shell.
Basic Usage
Add & at the end of a command:
#> long_running_command &
[1] 12345
#>
The shell prints:
- Job number in brackets
[1] - Process ID
12345 - Returns to prompt immediately
Job Control
List Jobs
#> jobs
[1] Running sleep 100 &
[2] Running make &
Bring to Foreground
#> fg %1
# Job 1 now in foreground
Send to Background
#> bg %1
# Continues job 1 in background
Stopping Jobs
Kill by Job Number
#> kill %1
Kill by PID
#> kill 12345
Job Completion
When a background job completes:
[1] Done sleep 10 &
Common Patterns
Run Long Command
make &
# Continue working while make runs
Multiple Background Jobs
command1 &
command2 &
command3 &
# All three run simultaneously
Redirect Output
long_command > output.log 2>&1 &
# Run in background, save output to file
Discard Output
noisy_command &> /dev/null &
Background Process Behavior
stdin
Background processes have stdin disconnected from terminal:
cat &
# Will receive EOF immediately
Signals
Background processes:
- Ignore
SIGINT(Ctrl+C) - Receive
SIGHUPwhen shell exits (unless usingnohup)
Exit Warning
When exiting with running jobs:
#> exit
hash: There are running jobs.
#> exit
# Second exit forces quit
Practical Examples
Build While Working
make clean && make &
# Edit files while building
Download in Background
curl -O http://example.com/large-file.zip &
Run Server
python -m http.server 8000 &
Watch Logs
tail -f /var/log/syslog &
Tips
Check if Job Running
jobs
# or
ps aux | grep command
Wait for Completion
command &
pid=$!
# Do other work...
wait $pid
echo "Command finished"
Nohup for Persistent Jobs
For jobs that should survive logout:
nohup long_command &
# Output goes to nohup.out
Limitations
- Job control is basic compared to bash
- No
disowncommand - No sophisticated job status tracking
Quick Reference
A consolidated reference for hash shell features.
Command Line Options
| Option | Description |
|---|---|
-c string | Execute commands from string |
-i | Force interactive mode |
-l, --login | Run as login shell |
-s | Read from stdin |
-v, --version | Print version |
-h, --help | Show help |
Key Bindings
Navigation
| Key | Action |
|---|---|
Left/Right | Move cursor |
Up/Down | History navigation |
Ctrl+A | Beginning of line |
Ctrl+E | End of line |
Home/End | Beginning/End of line |
Editing
| Key | Action |
|---|---|
Backspace | Delete before cursor |
Delete | Delete at cursor |
Ctrl+U | Delete to beginning |
Ctrl+K | Delete to end |
Ctrl+W | Delete word |
Other
| Key | Action |
|---|---|
Tab | Auto-complete |
Ctrl+L | Clear screen |
Ctrl+C | Cancel line |
Ctrl+D | EOF / Exit |
Operators
| Operator | Description |
|---|---|
| | Pipe stdout to stdin |
> | Output to file (overwrite) |
>> | Output to file (append) |
< | Input from file |
2> | Stderr to file |
&> | Both stdout and stderr |
2>&1 | Stderr to stdout |
&& | Run if previous succeeded |
|| | Run if previous failed |
; | Run sequentially |
& | Run in background |
Special Variables
| Variable | Description |
|---|---|
$? | Exit code of last command |
$$ | Shell process ID |
$0 | Shell/script name |
$1-$9 | Positional arguments |
$# | Number of arguments |
$@ | All arguments (separate) |
$* | All arguments (single) |
Test Operators
File Tests
| Test | Description |
|---|---|
-e | Exists |
-f | Regular file |
-d | Directory |
-r | Readable |
-w | Writable |
-x | Executable |
-s | Size > 0 |
-L | Symbolic link |
String Tests
| Test | Description |
|---|---|
-z | Empty string |
-n | Non-empty string |
= | Equal |
!= | Not equal |
Numeric Tests
| Test | Description |
|---|---|
-eq | Equal |
-ne | Not equal |
-lt | Less than |
-le | Less or equal |
-gt | Greater than |
-ge | Greater or equal |
PS1 Escape Sequences
| Sequence | Description |
|---|---|
\w | Full path |
\W | Current directory |
\u | Username |
\h | Hostname |
\g | Git branch + status |
\e | Exit code color |
\$ | $ or # |
\n | Newline |
History Expansion
| Pattern | Description |
|---|---|
!! | Last command |
!n | Command number n |
!-n | n commands ago |
!prefix | Last starting with prefix |
Environment Variables
| Variable | Description |
|---|---|
HISTSIZE | Commands in memory |
HISTFILESIZE | Commands in file |
HISTFILE | History file location |
HISTCONTROL | History behavior |
PS1 | Prompt format |
PATH | Command search path |
HOME | Home directory |
EDITOR | Default editor |
Built-in Commands
| Command | Description |
|---|---|
alias | Define/list aliases |
unalias | Remove alias |
cd | Change directory |
pwd | Print working directory |
echo | Print arguments |
export | Set environment variable |
source | Execute file in current shell |
exit | Exit shell |
logout | Exit login shell |
history | Show command history |
jobs | List background jobs |
fg | Bring job to foreground |
bg | Continue job in background |
kill | Send signal to process |
test | Evaluate expression |
[ | Same as test |
true | Return success |
false | Return failure |
Configuration Files
| File | When Loaded |
|---|---|
/etc/profile | Login shell |
~/.hash_profile | Login shell |
~/.profile | Login shell (fallback) |
~/.hashrc | Interactive shell |
~/.hash_logout | Login shell exit |
Contributing Guide
Thank you for your interest in contributing to hash!
Getting Started
Prerequisites
- GCC or Clang compiler
- Make build system
- Git
Fork and Clone
git clone https://github.com/YOUR_USERNAME/hash.git
cd hash
git remote add upstream https://github.com/juliojimenez/hash.git
Build
make clean
make
./hash-shell
Debug Build
make debug
Project Structure
hash/
├── src/ # Source files
│ ├── main.c # Entry point
│ ├── hash.h # Main header
│ ├── parser.c/h # Command parsing
│ ├── execute.c/h # Execution
│ ├── builtins.c/h # Built-in commands
│ ├── history.c/h # Command history
│ ├── completion.c/h # Tab completion
│ ├── lineedit.c/h # Line editing
│ ├── prompt.c/h # Prompt generation
│ └── ...
├── tests/ # Unit tests
├── docs/ # Documentation
├── book/ # Book of Hash source
└── Makefile
Coding Standards
Style
- 4 spaces indentation (no tabs)
- Functions:
snake_casewith module prefix - Constants:
UPPER_SNAKE_CASE - Max line length: 100 characters
int example_function(char *arg) {
if (arg == NULL) {
return -1;
}
return 0;
}
Safe String Functions
Always use safe string functions:
// Good
safe_strcpy(dst, src, sizeof(dst));
safe_strcat(dst, src, sizeof(dst));
// Avoid
strcpy(dst, src); // No bounds checking
strcat(dst, src); // No bounds checking
Compiler Warnings
Code must compile cleanly:
gcc -Wall -Wextra -Werror -std=gnu99
Testing
Setup Test Framework
make test-setup
Run Tests
make test # Unit tests
./test.sh # Integration tests
Writing Tests
Tests use Unity framework:
#include "unity.h"
void test_feature_works(void) {
int result = function("input");
TEST_ASSERT_EQUAL_INT(0, result);
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_feature_works);
return UNITY_END();
}
Submitting Changes
Branch Naming
feature/description- New featuresfix/description- Bug fixesdocs/description- Documentation
Commit Messages
type(scope): description
Types: feat, fix, docs, style, refactor, test, chore
Examples:
feat(completion): add environment variable completion
fix(history): prevent duplicate entries
docs(readme): update installation instructions
Before Submitting
# Sync with upstream
git fetch upstream
git rebase upstream/main
# Run tests
make clean && make && make test
./test.sh
# Check warnings
make CFLAGS="-Wall -Wextra -Werror -std=gnu99"
Pull Request
- Push branch to your fork
- Open PR against
juliojimenez/hash:main - Fill out PR template
- Address review feedback
Getting Help
- Questions: GitHub Discussions
- Bugs: GitHub Issues
- Email: julio@julioj.com
Testing
Hash uses the Unity test framework for unit tests and shell scripts for integration tests.
Setup
Download the Unity framework (first time only):
make test-setup
Running Tests
Unit Tests
make test
Integration Tests
./test.sh
Clean Test Artifacts
make test-clean
Writing Unit Tests
Test File Structure
Create tests/test_module.c:
#include "unity.h"
#include "../src/module.h"
void setUp(void) {
// Setup before each test
}
void tearDown(void) {
// Cleanup after each test
}
void test_feature_works(void) {
int result = module_function("input");
TEST_ASSERT_EQUAL_INT(0, result);
}
void test_handles_null(void) {
int result = module_function(NULL);
TEST_ASSERT_EQUAL_INT(-1, result);
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_feature_works);
RUN_TEST(test_handles_null);
return UNITY_END();
}
Common Assertions
TEST_ASSERT_EQUAL_INT(expected, actual)
TEST_ASSERT_EQUAL_STRING(expected, actual)
TEST_ASSERT_NOT_NULL(pointer)
TEST_ASSERT_NULL(pointer)
TEST_ASSERT_TRUE(condition)
TEST_ASSERT_FALSE(condition)
Integration Tests
Add tests to test.sh:
run_test "description" "command" "expected_output"
run_command_test "description" "command"
run_file_test "description" "command" "file" "expected"
Sanitizer Tests
AddressSanitizer
make clean
make CC=clang CFLAGS="-Wall -Wextra -O1 -g -fsanitize=address -fno-omit-frame-pointer -std=gnu99"
make test-clean
make test CC=clang CFLAGS="-Wall -Wextra -O1 -g -fsanitize=address -fno-omit-frame-pointer -std=gnu99"
UndefinedBehaviorSanitizer
make clean
make CC=clang CFLAGS="-Wall -Wextra -O1 -g -fsanitize=undefined -fno-omit-frame-pointer -std=gnu99"
make test-clean
make test CC=clang CFLAGS="-Wall -Wextra -O1 -g -fsanitize=undefined -fno-omit-frame-pointer -std=gnu99"
Combined (Most Thorough)
make clean
make CC=clang CFLAGS="-Wall -Wextra -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer -std=gnu99"
make test-clean
make test CC=clang CFLAGS="-Wall -Wextra -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer -std=gnu99"
Test Coverage Guidelines
When adding features:
- Write unit tests for all public functions
- Write integration tests for user-facing features
- Test edge cases (NULL, empty strings, boundaries)
- Test error handling paths
Security Policy
Supported Versions
The last 5 major versions receive security updates:
| Version | Supported |
|---|---|
| v34.x | Yes |
| v33.x | Yes |
| v32.x | Yes |
| v31.x | Yes |
| v30.x | Yes |
| < v30 | No |
Reporting Vulnerabilities
Do NOT report security vulnerabilities through public issues.
Report via: Settings > Security > Advisories > New draft security advisory
Include:
- Type of vulnerability
- Affected source files
- Steps to reproduce
- Proof-of-concept (if possible)
- Impact assessment
Response Timeline
- Acknowledgment: 48 hours
- Initial Assessment: 7 days
- Resolution: 30 days (critical)
Security Best Practices
For Users
- Keep hash updated
- Review .hashrc from untrusted sources
- Set proper permissions on config files (600)
- Use leading space for sensitive commands
For Contributors
- Use safe string functions (
safe_strcpy,safe_strcat) - Validate all array indices and buffer sizes
- Never trust user input
- Use reentrant functions (
getpwuid_rnotgetpwuid) - Free all allocated memory
- Pass SonarQube analysis
Security Features
Hash includes:
- Safe string utilities (bounds-checked)
- Input sanitization in parser
- History privacy (space prefix)
- No unsafe C functions (
gets,strcpy, etc.) - Stack protection (
-fstack-protector-strong) - Fortify source (
-D_FORTIFY_SOURCE=2)
Known Limitations
As a shell, hash executes arbitrary commands by design:
- Commands run with user privileges
- Aliases can execute arbitrary code
- Sourced scripts run in current context