Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
branches: [ master, main ]

jobs:
test:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -18,5 +18,22 @@ jobs:
- name: Run ShellCheck
run: shellcheck aliasme.sh test/*.sh

test:
# The script is sourced into the user's shell, so it has to be exercised
# under both supported shells; most users install via Homebrew on macOS,
# where zsh is the default.
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
shell: [bash, zsh]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

- name: Install zsh
if: matrix.shell == 'zsh' && runner.os == 'Linux'
run: sudo apt-get install -y zsh

- name: Run Tests
run: make test
run: ${{ matrix.shell }} test/aliastest.sh
45 changes: 27 additions & 18 deletions aliasme.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ _list() {
while IFS= read -r name
do
if ! IFS= read -r value; then break; fi
echo "$name : $value"
printf '%s : %s\n' "$name" "$value"
done < "$ALIASME_CMD"
fi
}
Expand Down Expand Up @@ -52,13 +52,12 @@ _add() {
fi

if _find "$name"; then
echo "$name already exists, remove it first: al rm $name"
printf '%s already exists, remove it first: al rm %s\n' "$name" "$name"
return 1
fi

echo "$name" >> "$ALIASME_CMD"
echo "$cmd" >> "$ALIASME_CMD"
echo "add: $name -> $cmd"
printf '%s\n%s\n' "$name" "$cmd" >> "$ALIASME_CMD"
printf 'add: %s -> %s\n' "$name" "$cmd"

_autocomplete
}
Expand All @@ -78,34 +77,44 @@ _remove() {
do
if ! IFS= read -r value; then break; fi
if [ "$line" = "$name" ]; then
echo "remove $name"
printf 'remove %s\n' "$name"
found=0
else
echo "$line" >> "$ALIASME_DIR/cmdtemp"
echo "$value" >> "$ALIASME_DIR/cmdtemp"
printf '%s\n%s\n' "$line" "$value" >> "$ALIASME_DIR/cmdtemp"
fi
done < "$ALIASME_CMD"
mv "$ALIASME_DIR/cmdtemp" "$ALIASME_CMD"
fi
if [ "$found" -ne 0 ]; then
echo "not found: $name"
printf 'not found: %s\n' "$name"
fi
_autocomplete
return "$found"
}

_excute() {
local name value
# Prefixed names: the stored command is eval'd in this scope, so plain
# names like "name" would shadow the user's own variables.
local _al_name _al_value _al_found
# zsh aborts on a glob that matches nothing, which would break any stored
# command containing a literal ? or * (a URL query string, typically).
if [ -n "$ZSH_VERSION" ]; then
setopt local_options no_nomatch
fi
_al_found=1
if [ -s "$ALIASME_CMD" ];then
while IFS= read -u9 -r name; do
if ! IFS= read -u9 -r value; then break; fi
if [ "$1" = "$name" ]; then
eval "$value"
return $?
while IFS= read -u9 -r _al_name; do
if ! IFS= read -u9 -r _al_value; then break; fi
if [ "$1" = "$_al_name" ]; then
_al_found=0
break
fi
done 9< "$ALIASME_CMD"
fi
return 1
[ "$_al_found" -eq 0 ] || return 1

# Run with the storage file closed so the command cannot inherit fd 9.
eval "$_al_value"
}

_bashauto()
Expand Down Expand Up @@ -167,13 +176,13 @@ al(){
echo "al -v # version information"
echo "al -h # help"
elif [ "$1" = "-v" ]; then
echo "aliasme 3.1.0"
echo "aliasme 3.1.1"
echo "visit https://github.com/Jintin/aliasme for more information"
else
if _find "$1"; then
_excute "$1"
else
echo "not found: $1"
printf 'not found: %s\n' "$1"
return 1
fi
fi
Expand Down
139 changes: 107 additions & 32 deletions test/aliastest.sh
Original file line number Diff line number Diff line change
@@ -1,45 +1,120 @@
#!/bin/bash

. test/assert.sh
. aliasme.sh
. ./test/assert.sh

testInit() {
if [[ ! -f ~/.aliasme/path ]]; then
mkdir -p ~/.aliasme && touch ~/.aliasme/path
fi
# Run against a throwaway store so the suite never touches the real ~/.aliasme
ALIASME_DIR=$(mktemp -d "${TMPDIR:-/tmp}/aliasme_test.XXXXXX")
export ALIASME_DIR
cleanup() {
rm -rf "$ALIASME_DIR"
}
trap cleanup EXIT

testAlias() {

name1=testaaa
cmd1=cmdaaa
testAdd "$name1" "$cmd1"

name2=testbbb
cmd2=cmdbbb
testAdd "$name2" "$cmd2"
# zsh's "." does not search the current directory, so keep the "./" prefix
. ./aliasme.sh

testRemove "$name1"
testRemove "$name2"
}
failures=0

testAdd() {
_add "$1" "$2"
if [[ $(_list) = *"$1 : $2"* ]]; then
log_success "path test success"
# Names are prefixed: assert.sh declares "expected"/"actual" as arrays.
check() {
local _desc="$1" _want="$2" _got="$3"
if [ "$_want" = "$_got" ]; then
log_success "$_desc"
else
log_failure "path test failure"
log_failure "$_desc -- expected [$_want] got [$_got]"
failures=$((failures + 1))
fi
}

testRemove() {
_remove "$1"
if [[ $(_list) = *"$1"* ]]; then
log_failure "remove test failure"
else
log_success "remove test success"
fi
reset_store() {
rm -f "$ALIASME_CMD"
}

testInit
testAlias
log_header "Add and list"

reset_store
_add hello "echo hello" > /dev/null
_add world "echo world" > /dev/null
check "list renders both entries" \
"hello : echo hello
world : echo world" "$(_list)"

check "duplicate name is rejected" "1" "$(_add hello "echo other" > /dev/null 2>&1; echo $?)"
# stdin is closed: with no arguments _add prompts for them interactively,
# and the suite must not block waiting on a terminal.
check "empty name is rejected" "1" "$(_add "" "" > /dev/null 2>&1 < /dev/null; echo $?)"

log_header "Remove"

reset_store
_add keep "echo keep" > /dev/null
_add drop "echo drop" > /dev/null
_remove drop > /dev/null
check "removed entry is gone" "keep : echo keep" "$(_list)"
check "removing an unknown name reports failure" "1" "$(_remove ghost > /dev/null 2>&1; echo $?)"

log_header "Execute"

reset_store
_add greet "echo hi" > /dev/null
check "alias runs its command" "hi" "$(al greet)"
check "unknown alias reports not found" "not found: nosuch" "$(al nosuch 2>&1)"
check "unknown alias exits non-zero" "1" "$(al nosuch > /dev/null 2>&1; echo $?)"

reset_store
_add fails "false" > /dev/null
check "failing command propagates its exit code" "1" "$(al fails > /dev/null 2>&1; echo $?)"
_add code "return 3" > /dev/null
check "exit code is passed through verbatim" "3" "$(al code > /dev/null 2>&1; echo $?)"

log_header "Name and command collision"

# A command line whose text equals another alias's name must never be
# mistaken for that name when executing or removing.
reset_store
_add x deploy > /dev/null
_add deploy "echo deploying" > /dev/null
check "lookup skips command lines" "deploying" "$(al deploy)"

_remove deploy > /dev/null
check "removal skips command lines" "x : deploy" "$(_list)"

log_header "Escapes and special characters"

reset_store
_add box 'printf "[%s]\n" done' > /dev/null
_add after "echo STILL_HERE" > /dev/null
check "backslash escapes are stored verbatim" \
'box : printf "[%s]\n" done
after : echo STILL_HERE' "$(_list)"
check "entry after an escaped command is still reachable" "STILL_HERE" "$(al after)"
check "escaped command runs correctly" "[done]" "$(al box)"

reset_store
_add url "echo http://api.com/x?key=1" > /dev/null
check "literal ? in a command survives" "http://api.com/x?key=1" "$(al url)"
check "literal ? does not fail the command" "0" "$(al url > /dev/null 2>&1; echo $?)"

# Suppressing the "no matches" error must not disable globbing itself.
reset_store
touch "$ALIASME_DIR/globbed.txt"
_add glob "echo $ALIASME_DIR/*.txt" > /dev/null
check "an intentional glob still expands" "$ALIASME_DIR/globbed.txt" "$(al glob)"

log_header "Variable isolation"

reset_store
# shellcheck disable=SC2016 # the command is stored literally, on purpose
_add show 'echo "[$name][$value][$line]"' > /dev/null
name=OUTER
value=OUTER
line=OUTER
check "aliasme internals do not shadow user variables" \
"[OUTER][OUTER][OUTER]" "$(al show)"

if [ "$failures" -gt 0 ]; then
log_failure "$failures test(s) failed"
exit 1
fi

log_success "All tests passed!"
Loading