Shell script languages (like bash, zsh ...) are widely used on Unix-like operating systems, including GNU/Linux and macOS.
Nevertheless, basic operations, such as split or trim, often end up being rewritten ... Again and again for different shells.
Apash is a library providing those kinds of operations (for strings, arrays, dates and more...). It is inspired from Apache's libraries developed in Java.
The core concepts of Apash are to enhance readibility and portability for basic operations.
Even though it could be a wish, Apash is not a project of the Apache Foundation.
Let's stop talking and open the shell !
StringUtils.rightPad "123" 6 "!"
# 123!!!
- Installation
- Quick Start
- Core Concepts
- Documentation
- Containers
- Compatibility (Matrix)
- Configuration
- Troubleshooting
- Maintenance
- License
- Explore the API (or with the Full Summary Table)
- How to contribute ?
A minified version of apash is available in order to facilitate the usage and improve performance.
Just download, source and use. This minified version exists for bash and zsh.
With this package, only the runtime library is available (not the apash command).
# Download version for bash (exists for zsh too, see ZSH variant below)
curl "https://raw.githubusercontent.com/hastec-fr/apash/refs/heads/main/bin/apash-bash-min.sh" -o apash-bash-min.sh
# Source
. ./apash-bash-min.sh
# Repeat the string
StringUtils.repeat 2 "ah! "
# result: ah! ah!
ZSH Variant
The same applies if you want to use it with zsh.curl "https://raw.githubusercontent.com/hastec-fr/apash/refs/heads/main/bin/apash-zsh-min.sh" -o apash-zsh-min.sh
. ./apash-zsh-min.sh
StringUtils.upperCase "Hello World"
As other shell projects, there is no standard way to install Apash.
Below are the main ones.
Install curl and git to proceed with the scripted installation.
# Adapt the command with your own package manager (here apt).
sudo apt install curl git
Modify the URL accordingly if you want a particular version. In that case, it's for the head of the main branch (for BASH):
curl -s "https://raw.githubusercontent.com/hastec-fr/apash/refs/heads/main/utils/install.sh" | bash
Open a new terminal and check the apash version:
apash --version
# 0.2.0
ZSH Variant
Please note that "| zsh" is important to overload the default shebang (bash).
curl -s "https://raw.githubusercontent.com/hastec-fr/apash/refs/heads/main/utils/install.sh" | zsh
Open a new terminal and check the apash version:
apash --version
# 0.2.0
Installation with Basher
Basher is a package manager for bash which helps you to quickly install, uninstall and update bash packages from the command line.
curl -s "https://raw.githubusercontent.com/basherpm/basher/master/install.sh" | bash
basher install "hastec-fr/apash"
export APASH_HOME_DIR="$HOME/.basher/cellar/packages/hastec-fr/apash" && APASH_SHELL=bash "$APASH_HOME_DIR/apash" init --post-install
Open a new terminal to ensure that environment is refreshed with apash functions.
Clone or download the Apash project, execute the post installation action to add apash sourcing to your startup script file (like $HOME/.bashrc).
# First select where apash should be installed
APASH_HOME_DIR="$HOME/.apash"
git clone "https://github.com/hastec-fr/apash.git" "$APASH_HOME_DIR"
cd "$APASH_HOME_DIR"
# Post installation actions:
# Note: Post install update the startup script with the desired $APASH_HOME_DIR.
export APASH_HOME_DIR="path/to/apash"
./apash init --post-install
# At the end, open a new terminal to ensure that environnment is re-loaded.
For testing purposes, shellspec must be installed.
Please refer to their installation section for any changes
curl -fsSL https://git.io/shellspec | sh -s -- --yes
echo "PATH=\$PATH:\$HOME/.local/bin" >> "$HOME/.bashrc" # Add shellspec to the path. $HOME/.zshrc for zsh.
Basher
basher install "shellspec/shellspec"
Bpkg
bpkg install "shellspec/shellspec"
Once Apash is installed, you can easily use a function by importing it.
In order to do that, use the function "apash.import" with the name of the function.
apash.import "fr.hastec.apash.commons-lang.StringUtils.substring"
StringUtils.substring "Hello World" 0 4
Do you see the π₯Hellπ₯in the sHell ? It's only the beginning.
If it's not the case, let's have a look at the troubleshooting section.
Please refer to the full summary Table to get the list of all the available functions. Note that functions can be directly used without import with minified version.
One of the initial objectives of Apash was to make basic operations more readable.
Indeed, shell languages use many symbols and shortcuts. It looks great for advanced shell programmers and harder to read for the others.
Example:
# I want to count the number of "a" in the word "apash".
# For this operation, I need to know 3 commands, 2 options and how pipes work.
echo "apash" | grep -o "a" | wc -l
# Here, we have an idea of what the function does.
StringUtils.countMatches "apash" "a"
# result: 2
If the SHELL could be defined as a language, there would be many dialects (bash, zsh, ksh...) which would not be compatible with each other.
The POSIX standard exists for this purpose and if a script (and SHELL) follows that standard then you can be sure that it will work.
It's nice, but writing a script following POSIX standard is not easy and quickly requires high skills and knowledge of the standard (no array, less parameter expansions...).
So Apash approaches the problem from the other end. It allows development in a dialect with all features and provides a mechanism of variants for incompatible codes.
Example:
# The code below shows how to vary the code for rendering a string in UPPERCASE.
# If zsh, then use the corresponding parameter expansion.
if [ "$APASH_SHELL" = "zsh" ]; then
echo "${(U)inString}"
# Else if it's bash with version greater than 4.2, then use another parameter expansion.
elif [ "$APASH_SHELL" = "bash" ] && \
! VersionUtils.isLowerOrEquals "$APASH_SHELL_VERSION" "4.2"; then
echo "${inString^^}"
# Otherwise, use a more posix way to transform the string.
else
echo "$inString" | awk '{print toupper($0)}'
fi
This can be done inside a single file but it could be segregated per file.
lowerCase.sh # Default script having the function to lowercase a string (generally latest bash version).
lowerCase.zsh # Variant for zsh
lowerCase.bash_4.2 # Variant for bash equals or less than the version 4.2: ${inString,,} appears with the 4.3.
This mechanism allows extending to other shells (ksh, csh, dash...) and sharing a maximum of compatible code in same time.
Today, the library is really not POSIX (just bash and zsh), but people knowing other shells can contribute with their own dialect.
The POSIX form of the functions could be imagined in the future with .posix files (with a wrapping mechanism for function names currently using dots).
Documentation is generated from to the script comments.
Each file contains a header following a template in order to generate the corresponding markdown file (.md) in the doc directory.
The template of the documentation is available in assets/template/apashCommentTemplate.sh.
To generate the documentation, execute the command:
apash doc
The latest version of this documentation is available in the wiki of the project.
You're in a terminal, apash provides a feature to display a summary of the markdown.
apash help "lowercase"
# @file $APASH_HOME_DIR/src/fr/hastec/apash/commons-lang/StringUtils/lowerCase.sh
# @name StringUtils.lowerCase
# @brief Converts a String to lower case.
# ### Arguments
# # | Type | Description
#--------|---------------|---------------------------------------
# $1 | string | The string to lower case.
#
It is possible to search by:
- The file path.
- The Class.Method.
- Or just the method (but it takes the first match).
Apash tests are executed with the tool shellspec (ref. dependencies). Once installed, launch the tests campaign from the root project directory.
apash test
You can also override the shellspec options and choose specific tests as in this example:
# Prototype: apash test [-h] [--test-options options] [--] [test paths]
apash test --test-options "--directory $APASH_HOME_DIR --shell bash --format tap" $APASH_HOME_DIR/spec/fr/hastec/apash/lang/Math/*_spec.sh
Note that shellspec options are in a single argument.
If you don't like the long name, you can create your own function aliases (as usual).
alias import="apash.import"
alias trim="StringUtils.trim"
import fr.hastec.apash.commons-lang.StringUtils.trim
trim " This is the end. "
Just keep in mind, that aliases are useful for your prompt but (depending of the shell) they cannot be exported. By example for bash, you should declare again the aliases from subscript or activate the shell option on your own:
shopt -s expand_aliases
If you don't want to install apash but test it quickly, you can pull its containers on docker hub. Default is bash, but you can get zsh too.
docker run --rm hastec/apash:0.2.0 # run with bash (5.2)
docker run --rm hastec/apash:0.2.0-zsh # run with zsh (5.9)
docker run --rm hastec/apash:0.2.0-bash-ready # run with bash (5.2) and functions are already imported.
# Example:
docker run --rm -it hastec/apash:0.2.0-bash
apash:bash-5.2 $ echo $BASH_VERSION
# 5.2.32(1)-release
docker run --rm hastec/apash:0.2.0 '
apash.import "fr.hastec.apash.commons-lang.StringUtils"
StringUtils.reverse "Never odd or even!"
'
Result:
!neve ro ddo reveN
If you don't like to import the command yourself, then use the image with all functions ready to use:
docker run --rm hastec/apash:0.2.0-ready 'StringUtils.upperCase "Please, speak louder !!"'
Result:
PLEASE, SPEAK LOUDER !!
Finally, if you want to test a script, use the image and mount the script as volume. Make sure to provide an absolute host path (not relative).
cat <<EOF > ./test.sh
apash.import fr.hastec.apash.commons-lang.StringUtils.abbreviate
StringUtils.abbreviate "Thanks to abbreviate this long description which does not lead anywhere except to pretend that this function could have a use case." 15
EOF
docker run --rm -v "$PWD/test.sh:/home/apash/test.sh:ro" hastec/apash:0.2.0 ./test.sh
Result:
Thanks to ab...
Modify your apash installation and test non regression using containers.
# From root apash workspace directory ($APASH_HOME_DIR)
docker build -t docker.io/hastec/apash:0.2.0 -f ./docker/apash-bash.dockerfile .
docker run --rm hastec/apash:0.2.0 'apash test'
Apash provides a way to build and run a container for a particular shell (bash/zsh) and its version.
Ideal for testing:
# Create and run the image with current context($APASH_HOME_DIR).
# The image is named as following: hastec/apash-local:<version>-<shell>_<version>
apash docker --shell "zsh" --version "5.9" # hastec/apash-local:0.2.0-zsh_5.9
apash docker -s "bash" -v "5.0" # hastec/apash-local:0.2.0-bash_5.0
A compatibility matrix is available.
The scope of this matrix is on the functions of the library (directory: src/fr/hastec/apash), not on the tools around (doc test...) where it's recommended to use latest shell versions when you develop new features.
Currently, the library (not tools) is compatible from bash version 5.2 to 4.3 (2014-02-26)
Issues appear at the version 4.2 (2011-02-13) and older.
This is essentially due the nameref statement (local -n) which is a key feature appearing with the version 4.3.
This feature prevents the call of the evil (I mean eval. Sorry it seems to be running gag, so I did it).
It is not planned to have a workaround for the moment but if it is desired to use apash under this version
or some people are motivated π to create a .posix version, the evil could be welcome and available only for these specific versions.
References:
- Bash release version table from Wikipedia.
Currently, the library (not tools) is compatible from bash version 5.9 to 5.3 (2016-12-13)
As nameref, the (P) modifier has been introduced in 5.3.
References:
APASH variables which can be adjusted are present in the file $APASH_HOME_DIR/.apashrc.
When the minified version is used, it's on the top of the minified file delimited by a line of hastag.
By default, apash logs the unexpected errors but it could be adjusted to different levels.
# Levels:
APASH_LOG_LEVEL_OFF=0
APASH_LOG_LEVEL_FATAL=100
APASH_LOG_LEVEL_ERROR=200
APASH_LOG_LEVEL_WARN=300
APASH_LOG_LEVEL_INFO=400
APASH_LOG_LEVEL_DEBUG=500
APASH_LOG_LEVEL_TRACE=600
APASH_LOG_LEVEL_ALL="$Integer_MAX_VALUE"
# Default valye. It displays WARNING and lower levels (ERROR/FATAL)
APASH_LOG_LEVEL="${APASH_LOG_LEVEL:-$APASH_LOG_LEVEL_WARN}"
# To disable logs:
APASH_LOG_LEVEL="$APASH_LOG_LEVEL_OFF"
If you need to trace what is happening during Apash calls, it is recommended to increase the log level to trace, instead of using "set -x".
A trace has been put to each entry in and exit in order to keep control on the stack (potentially for @nnotations later).
APASH_LOG_LEVEL="$APASH_LOG_LEVEL_TRACE"
apash.import fr.hastec.apash.lang.Math.abs
Math.abs -3
# Result:
# 2024-11-22T16:25:50.286+0100 [TRACE] Math.abs (2): In Math.abs '-3'
# 2024-11-22T16:25:50.309+0100 [TRACE] NumberUtils.isParsable (2): In NumberUtils.isParsable '-3'
# 2024-11-22T16:25:50.333+0100 [TRACE] NumberUtils.isParsable (6): Out
# 3
# 2024-11-22T16:25:50.357+0100 [TRACE] Math.abs (7): Out
Here is a simple example but the stack could become very verbose too.
So a system of black/white lists exists in order to select which log could be output.
APASH_LOG_LEVEL="$APASH_LOG_LEVEL_TRACE"
apash.import fr.hastec.apash.commons-lang.ArrayUtils.add
ArrayUtils.add "myArray" Hello
# Logs with ArrayUtils.nullToEmpty, ShellUtils.isVariableNameValid, ShellUtils.isDeclared...
unset myArray
APASH_LOG_BLACKLIST+="ShellUtils.isVariableNameValid:ShellUtils.isDeclared"
ArrayUtils.add "myArray" Hello
# Logs without ShellUtils.isVariableNameValid and ShellUtils.isDeclared are displayed.
You can combine the black list (checked first) with the white list to restrict a maximum of logs.
unset myArray
APASH_LOG_WHITELIST+="ArrayUtils.add:ArrayUtils.isArray"
ArrayUtils.add "myArray" Hello
# Only logs of functions ArrayUtils.add and ArrayUtils.isArray are displayed.
If you're using ZSH, please check that $APASH_HOME_DIR is well defined.
If not, the script install.sh has may be directly executed after download without specifying the shell. So the shebang of the script (bash) has been used instead of zsh. It is suggested to cleanup the $HOME/.apash and remove lines from $HOME/.bashrc and reinstall taking care of the "| zsh" (or zsh -c "./install.sh").
The way to import scripts with desired shell/version takes times due to the recusive resolution. To reduce this time a cache has been put in place. It directly provides the list of scripts to source and only the shell/version are resolved. So if the first import is very long, it means the cache is certainly not used. Check first that $APASH_HOME_DIR/cache has some cache files inside.
find $APASH_HOME_DIR/cache -name "*.cache"
Then if it's a new function, you have created its cache:
find $APASH_HOME_DIR/cache -name "myFunction.cache"
# if not, create it:
apash cache "fr.hastec.apash.path.to.function"
If the problem still persists, I invite you to log an issue with your Apash environment variables and the location of the installation. One workaround is to use the minified version which preloads everything in one shot.
Some Apash Warnings could appear if you do not have a particular command (like "bc" or "rev").
In this case, another code flow is implemented (and works) but it notifies that the main flow is not followed.
The degraded mode could be less efficient (or could be different if you're playing with bounds).
So it is preferable to install the missing commands.
Nevertheless, you can disable these warnings by modifying the following variable in the configuration file (but keep in mind you disabled it !!).
export APASH_LOG_WARNING_DEGRADED="false"
The value can be modified in $APASH_HOME_DIR/.apashrc or directly in your environment.
You can check at any moment (even when degrade mode disabled) if some commands are missing (like with git bash) with the command:
apash check
2024-11-22T16:00:27.303+0100 [WARN] apash.check (34): **DEGRADED MODE** bc command not found.
2024-11-22T16:00:27.324+0100 [WARN] apash.check (35): **DEGRADED MODE** rev command not found.
By default, Apash returns an exception when something was unexpected. It can follow this form:
unset myArray myOtherArray
ArrayUtils.isSameLength myArray myOtherArray
2024-11-22T16:06:44.909+0100 [ERROR] ArrayUtils.isSameLength (5): Exception
at ArrayUtils.isSameLength($APASH_HOME_DIR/src/fr/hastec/apash/commons-lang/ArrayUtils/isSameLength.sh:5)
# <Timestamp with TZ> [Level] <Function> (<relative row>): Exception <Argument if any (nothing for the moment)>
# at <first function level>(<source path>:<relative row>)
The relative row relates to the row inside the function. Sorry, but for the time being, there is no offset to get the absolute row.
A trick is just to copy/paste the function in a blank source to have the correct offset.
1 ArrayUtils.isSameLength() {
2 Log.in $LINENO "$@"
3 local apash_inArrayName1="${1:-}"
4 local apash_inArrayName2="${2:-}"
5 ArrayUtils.isArray "$apash_inArrayName1" || { Log.ex $LINENO; return "$APASH_FAILURE"; } # <--- The error.
6 ArrayUtils.isArray "$apash_inArrayName2" || { Log.ex $LINENO; return "$APASH_FAILURE"; }
So the error has been raised because the first input was not an array. To get more traces, please refer to the logs section.
The "apash.import" manages which function should be sourced with its dependencies. It prevents cycling sourcing and useless re-sourcing (if already sourced). It's possible to force the reload of a library and its dependencies with option -f.
# -f: Force
apash.import -f "path.to.the.library"
If one new dependency is not sourced again, then it means you have added it to the list of import. The import uses a cache to prevent dependency recalculation. You can disable this cache (but it's slower) as follows:
# -n: No cache
apash.import -f -n "path.to.the.library"
# Rebuild the cache if necessary:
apash cache "path.to.the.library"
If there are any doubts on what is imported, it's possible to trace it:
# -s: Show import
apash.import -f -n -s "path.to.the.library"
Note that options should be set in the alphabetic order (-f -n -s).
Double check that the bind mount has an absolute path (not a relative one which does not work everywhere).
# Example of issue:
docker run --rm -v "./test.sh:/home/apash/test.sh:ro" hastec/apash:0.2.0 ./test.sh
# bash: line 1: ./test.sh: Is a directory
# docker run --rm -v "/absolute/path/to/test.sh:/home/apash/test.sh:ro" hastec/apash:0.2.0 ./test.sh
# For pseudo relative path, you can use the $PWD variable
docker run --rm -v "$PWD/test.sh:/home/apash/test.sh:ro" hastec/apash:0.2.0 ./test.sh
If you're playing with days around the daylight saving, you could have some troubles. Please refer to the GNU core FAQ.
Just pull out the latest version of the main branch.
git checkout main
git pull
Basher
The lastest version from main branch of github is pulled. ```bash basher upgrade hastec-fr/apash ``` #### Others ```bash "$APASH_HOME_DIR/utils/uninstall.sh" ```It recursively removes the directory $APASH_HOME_DIR and lines in the startup script (.bashrc).
"$APASH_HOME_DIR/utils/uninstall.sh"
Basher
```bash basher uninstall "hastec-fr/apash" ```Then remove the lines with #apashInstallTag from your profile.
# Example:
sed -i '/apashInstallTag/d' "$HOME/.bashrc"
Apash is a free and open-source software licensed under the Apache License Version 2.0 License. Please see the LICENSE.txt file for details.