Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

OptionDescription
-c stringExecute commands from string
-iForce interactive mode
-l, --loginRun as a login shell
-sRead commands from standard input
-v, --versionPrint version information
-h, --helpShow 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

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

PlatformMethodCommand
macOSHomebrewbrew install juliojimenez/hash/hash-shell
Debian/UbuntuAPTSee APT instructions
All platformsFrom sourcemake && sudo make install
DockerPull imagedocker 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

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

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/ to hash-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

DistributionCodenameArchitecture
Ubuntu 24.04nobleamd64
Ubuntu 22.04jammyamd64
Debian 12bookwormamd64
Debian 13trixieamd64

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

OptionDescription
-c stringExecute commands from string
-iForce interactive mode
-l, --loginRun as a login shell
-sRead commands from standard input
-v, --versionPrint version information
-h, --helpShow 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

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

OptionValuesDescription
colorson/offEnable colored output
welcomeon/offShow welcome message
PS1formatCustom 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

SequenceDescription
\wFull current path
\WCurrent directory name
\uUsername
\hHostname
\gGit branch with status
\eExit code color
\$$ for user, # for root
\nNewline

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:

  1. argv[0] starts with - (how SSH/login invoke shells)
  2. --login or -l flag is used

Login shells display “(login)” in the welcome message.

Startup File Order

Login Shell

When invoked as a login shell:

  1. /etc/profile - System-wide settings
  2. User profile (first found):
    • ~/.hash_profile
    • ~/.hash_login
    • ~/.profile
  3. ~/.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

FeatureDescription
Line EditingCursor navigation, editing shortcuts
Tab CompletionComplete commands, files, directories
Command HistoryNavigate and search previous commands
Prompt CustomizationGit integration, colors, custom format

Quick Reference

Line Editing

KeyAction
Ctrl+AMove to beginning of line
Ctrl+EMove to end of line
Ctrl+UDelete to beginning
Ctrl+KDelete to end
Ctrl+WDelete word backward
Ctrl+LClear screen
Ctrl+CCancel current line

History Navigation

KeyAction
UpPrevious command
DownNext command
!!Repeat last command
!nRun command number n
!prefixRun last command starting with prefix

Tab Completion

ContextCompletes
First wordCommands, aliases, builtins
Other wordsFiles, directories
Double TABShow 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

KeyAction
Left ArrowMove cursor left
Right ArrowMove cursor right
Up ArrowPrevious command in history
Down ArrowNext command in history
Ctrl+AMove to beginning of line
Ctrl+EMove to end of line
HomeMove to beginning of line
EndMove to end of line

Editing

KeyAction
BackspaceDelete character before cursor
DeleteDelete character at cursor
Ctrl+HSame as Backspace
Ctrl+UDelete from cursor to beginning
Ctrl+KDelete from cursor to end
Ctrl+WDelete word backward

Other

KeyAction
TabAuto-complete
Ctrl+LClear screen
Ctrl+CCancel current line
Ctrl+DEOF (exit if line empty)
EnterSubmit 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

FeatureHashBash
Command completionYesYes
File/directory completionYesYes
Alias completionYesYes
Path completionYesYes
Double-TAB shows matchesYesYes
Variable completionNoYes
Programmable completionNoYes

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 | 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

FeatureHashBash
!!YesYes
!nYesYes
!-nYesYes
!prefixYesYes
Up/Down arrowsYesYes
History fileYesYes
Privacy (space prefix)YesYes
Ctrl+RNoYes
!?patternNoYes
^old^newNoYes

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

SequenceDescriptionExample
\wFull current path/home/user/projects
\WCurrent directory onlyprojects

User Information

SequenceDescription
\uUsername
\hHostname
\$$ for user, # for root

Git Integration

SequenceDescription
\gGit 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

SequenceDescription
\eSets 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

SequenceDescription
\nNewline
\\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 clean
  • git: is yellow when dirty
  • Branch name is cyan

Exit Code Colors

  • \e sets blue on success (exit 0)
  • \e sets 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

VariableDescription
$0Script name
$1-$9Positional 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 - Success
  • 1 - General error
  • 2 - Misuse of command

Comments

# This is a comment
echo "Hello"  # Inline comment

Best Practices

  1. Always quote variables: "$var" prevents word splitting
  2. Use meaningful names: FILE_COUNT not fc
  3. Check for errors: Test return codes
  4. Add comments: Explain complex logic
  5. Use exit codes: Return 0 for success
  6. 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

TestDescription
-e fileFile exists
-f fileIs a regular file
-d fileIs a directory
-r fileIs readable
-w fileIs writable
-x fileIs executable
-s fileHas size > 0
-L fileIs 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

TestDescription
-z stringString is empty
-n stringString is not empty
s1 = s2Strings are equal
s1 != s2Strings 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

TestDescription
n1 -eq n2Equal
n1 -ne n2Not equal
n1 -lt n2Less than
n1 -le n2Less than or equal
n1 -gt n2Greater than
n1 -ge n2Greater 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

OperatorDescription
! exprNOT
expr -a exprAND
expr -o exprOR

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

OperatorTypeDescription
|PipeConnect stdout to stdin
>RedirectOutput to file (overwrite)
>>RedirectOutput to file (append)
<RedirectInput from file
2>RedirectStderr to file
&>RedirectBoth stdout and stderr
&&ChainRun if previous succeeded
||ChainRun if previous failed
;ChainRun sequentially
&BackgroundRun 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 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
  • command1 stdout → command2 stdin
  • command2 stdout → command3 stdin
  • command3 stdout → 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 ||

OperatorPurpose
|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 = stdin
  • 1 = stdout
  • 2 = 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

OperatorNameBehavior
&&ANDRun next if previous succeeded (exit 0)
||ORRun next if previous failed (exit non-0)
;SequentialRun 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

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 SIGHUP when shell exits (unless using nohup)

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 disown command
  • No sophisticated job status tracking

Quick Reference

A consolidated reference for hash shell features.

Command Line Options

OptionDescription
-c stringExecute commands from string
-iForce interactive mode
-l, --loginRun as login shell
-sRead from stdin
-v, --versionPrint version
-h, --helpShow help

Key Bindings

KeyAction
Left/RightMove cursor
Up/DownHistory navigation
Ctrl+ABeginning of line
Ctrl+EEnd of line
Home/EndBeginning/End of line

Editing

KeyAction
BackspaceDelete before cursor
DeleteDelete at cursor
Ctrl+UDelete to beginning
Ctrl+KDelete to end
Ctrl+WDelete word

Other

KeyAction
TabAuto-complete
Ctrl+LClear screen
Ctrl+CCancel line
Ctrl+DEOF / Exit

Operators

OperatorDescription
|Pipe stdout to stdin
>Output to file (overwrite)
>>Output to file (append)
<Input from file
2>Stderr to file
&>Both stdout and stderr
2>&1Stderr to stdout
&&Run if previous succeeded
||Run if previous failed
;Run sequentially
&Run in background

Special Variables

VariableDescription
$?Exit code of last command
$$Shell process ID
$0Shell/script name
$1-$9Positional arguments
$#Number of arguments
$@All arguments (separate)
$*All arguments (single)

Test Operators

File Tests

TestDescription
-eExists
-fRegular file
-dDirectory
-rReadable
-wWritable
-xExecutable
-sSize > 0
-LSymbolic link

String Tests

TestDescription
-zEmpty string
-nNon-empty string
=Equal
!=Not equal

Numeric Tests

TestDescription
-eqEqual
-neNot equal
-ltLess than
-leLess or equal
-gtGreater than
-geGreater or equal

PS1 Escape Sequences

SequenceDescription
\wFull path
\WCurrent directory
\uUsername
\hHostname
\gGit branch + status
\eExit code color
\$$ or #
\nNewline

History Expansion

PatternDescription
!!Last command
!nCommand number n
!-nn commands ago
!prefixLast starting with prefix

Environment Variables

VariableDescription
HISTSIZECommands in memory
HISTFILESIZECommands in file
HISTFILEHistory file location
HISTCONTROLHistory behavior
PS1Prompt format
PATHCommand search path
HOMEHome directory
EDITORDefault editor

Built-in Commands

CommandDescription
aliasDefine/list aliases
unaliasRemove alias
cdChange directory
pwdPrint working directory
echoPrint arguments
exportSet environment variable
sourceExecute file in current shell
exitExit shell
logoutExit login shell
historyShow command history
jobsList background jobs
fgBring job to foreground
bgContinue job in background
killSend signal to process
testEvaluate expression
[Same as test
trueReturn success
falseReturn failure

Configuration Files

FileWhen Loaded
/etc/profileLogin shell
~/.hash_profileLogin shell
~/.profileLogin shell (fallback)
~/.hashrcInteractive shell
~/.hash_logoutLogin 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_case with 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 features
  • fix/description - Bug fixes
  • docs/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

  1. Push branch to your fork
  2. Open PR against juliojimenez/hash:main
  3. Fill out PR template
  4. Address review feedback

Getting Help

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:

VersionSupported
v34.xYes
v33.xYes
v32.xYes
v31.xYes
v30.xYes
< v30No

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

  1. Keep hash updated
  2. Review .hashrc from untrusted sources
  3. Set proper permissions on config files (600)
  4. Use leading space for sensitive commands

For Contributors

  1. Use safe string functions (safe_strcpy, safe_strcat)
  2. Validate all array indices and buffer sizes
  3. Never trust user input
  4. Use reentrant functions (getpwuid_r not getpwuid)
  5. Free all allocated memory
  6. 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