-
Notifications
You must be signed in to change notification settings - Fork 3
Home
If the command bashbud
is executed without any arguments,
an error message is printed:
$ bashbud
[ERROR] --template '' not found.
usage: bashbud [--template TEMPLATE] [TARGET_DIR]
available templates in ~/.config/bashbud:
bud
CLEANUP
default
ERR
LOG
mini
MSG
readme
TIMER
watch
Below is a list of files included in the default template.
~/.config/bashbud/default/
docs/
options/
help
version
.gitignore
config.mak
main.sh
Makefile
options
The command bashbud --template default MyScript
will create a directory called MyScript, and
the files from ~/.config/bashbud/default
will get
copied into this new MyScript/
directory.
main.sh
will get renamed to MyScript
.
And last make
will execute the Makefile.
Resulting in something like this:
MyScript/
docs/
options/
help
version
.cache/ (generated by make)
options/
help
version
getopt
help_table.md
help_table.txt
long_help.md
options_in_use
print_help.sh
.gitignore
_init.sh (generated by make)
_MyScript (generated by make)
config.mak
MyScript (this is just main.sh renamed)
Makefile
options
In the Makefile there is a short embedded AWK script that parses the content of the options file.
$ cat MyScript/options
--help|-h
--version|-v
Peeking at the content of .cache/options_in_use
,
you will see that it is just one line of the two long-option names
separated by spaces. ( help version
).
_init.sh
contains two functions (__print_version
, and __print_help
),
together with a generated getopt
loop, and lastly a call to: main "$@"
.
If options are added, removed or changed in the options file, it will be reflected in this file.
As a test, we can add the following line to options:
--cool-option1 --option-with-arg ROCKNROLL
If we now execute make
in the MyScript
directory, __print_help
and the getopt
loop
in _init.sh
will look like this:
__print_help()
{
cat << 'EOB' >&3
usage: MyScript [OPTIONS]
--cool-option1 | short description
-v, --version | print version info and exit
-h, --help | print help and exit
--option-with-arg ROCKNROLL | short description
EOB
}
declare -A _o
options=$(getopt \
--name "[ERROR]:MyScript" \
--options "v,h" \
--longoptions "cool-option1,version,help,option-with-arg:" -- "$@"
) || exit 98
eval set -- "$options"
unset options
while true; do
case "$1" in
--help | -h ) __print_help && exit ;;
--version | -v ) __print_version && exit ;;
--cool-option1 ) _o[cool-option1]=1 ;;
--option-with-arg ) _o[option-with-arg]=$2 ; shift ;;
-- ) shift ; break ;;
* ) break ;;
esac
shift
done
Notice that the description for both options is:
"short description". This text is taken from the
corresponding files in docs/options/
. To test, we can
change the content of docs/options/cool-option1
to:
if set all will be cool.
Also take note that the getopt loop,
where --option-with-arg
is expecting an argument,
while --cool-option1
is not.
Executing the script like this:
./MyScript --cool-option1 --option-with-arg BUDLABS
Will populate the global _o
array like this:
_o[cool-option1]=1
_o[option-with-arg]=BUDLABS
the name of the global array "_o". can be changed by setting the variable OPTIONS_ARRAY_NAME in
config.mak
If we change
--options-with-arg ROCKNROLL
to
--options-with-arg|-o
(removing the argument, adding short option)
in the options file. And execute make
again.
We will see the changes in _init.sh
:
__print_help()
{
cat << 'EOB' >&3
usage: MyScript [OPTIONS]
--cool-option1 | if set all will be cool.
-v, --version | print version info and exit
-h, --help | print help and exit
-o, --options-with-arg | short description
EOB
}
declare -A _o
options=$(getopt \
--name "[ERROR]:MyScript" \
--options "v,h,o" \
--longoptions "cool-option1,version,help,options-with-arg" -- "$@"
) || exit 98
eval set -- "$options"
unset options
while true; do
case "$1" in
--help | -h ) __print_help && exit ;;
--version | -v ) __print_version && exit ;;
--cool-option1 ) _o[cool-option1]=1 ;;
--options-with-arg | -o ) _o[options-with-arg]=1 ;;
-- ) shift ; break ;;
* ) break ;;
esac
shift
done
Now lets remove --option-with-arg
completely from
the option file and execute make
again.
__print_help()
{
cat << 'EOB' >&3
usage: MyScript [OPTIONS]
--cool-option1 | if set all will be cool.
-v, --version | print version info and exit
-h, --help | print help and exit
EOB
}
declare -A _o
options=$(getopt \
--name "[ERROR]:MyScript" \
--options "v,h" \
--longoptions "cool-option1,version,help" -- "$@"
) || exit 98
eval set -- "$options"
unset options
while true; do
case "$1" in
--help | -h ) __print_help && exit ;;
--version | -v ) __print_version && exit ;;
--cool-option1 ) _o[cool-option1]=1 ;;
-- ) shift ; break ;;
* ) break ;;
esac
shift
done
As expected, the references to --option-with-arg
is
removed from _init.sh
. But the files docs/options/option-with-arg
and
.cache/options/option-with-arg
is left. This is intentional,
so you can remove/re-apply options without the need to rewrite
the documentation.
The last two lines in MyScript
are important:
__dir=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") #bashbud
source "$__dir/_init.sh" #bashbud
First line sets the variable __dir
to the directory
of the script (resolved symlinks). Next line, simply
source
the _init.sh
file we looked at in the section
above.
The
__dir
variable is intended for internalbashbud
use only, do never rely on it in your own functions, and do never overwrite or unset it.
remember the last line in _init.sh
is just a call
to main "$@"
. The main()
is located in MyScript
.
That's how the script works, you execute, ./MyScript
, which
in turn source
_init.sh
and parses the command-line
with getopt
and lastly call main
in MyScript
for
the actual execution of the script. With the exception
if either --version
or --help
are set, in that case
__print_version
or __print_help
will be called and
the script terminated. Or if getopt
see incorrect
options passed, in which case an error message will get
printed.
If we look at _MyScript
, we will see that it
basically is the two files MyScript
and
_init.sh
neatly concatenated. But the last two
lines from MyScript
, mentioned above are not
present. This is because they end with the
comment #bashbud
. Lines ending like that will
never be included in the concatenated version
(_MyScript
) of the script.
If the first line of a file ends with '#bashbud' that whole file will be ignored and not included in _MyScript
And as it is now, there is no difference from a users
perspective to execute, MyScript
vs _MyScript
.
Lets create a new directory and a new file:
mkdir func
echo '# tell em!' > func/tellem.sh
The name of the file can be anything, it doesn't matter, but the content must be valid bash. To start We just add a comment to the file.
the name of the directory "func" however, must be "func". But you can change it by setting the variable FUNCS_DIR in
config.mak
Now, execute make
again, and the content of _init.sh
and _MyScript
will be slightly different.
In _init.sh
just before the getopt loop, the following
lines are added:
for ___f in "$__dir/func"/*; do
. "$___f" ; done ; unset -v ___f
source (.
) each file in func/
(currently only our
just created tellem.sh
).
Looking into _MyScript
we will see that in between
__print_help()
and the getopt loop,
the content (# tell em!
) of the file.
The source loop in _init.sh
uses a wildcard match,
so we can add more files to func/
and they will be
automatically picked up by the loop without us (or the Makefile)
changing _init.sh
. _MyScript
however, needs to be rebuilt,
to reflect the changes.
Lets add a simple function in func/tellem.sh
:
#!/bin/bash
tellem() {
echo "We're cool"
}
And just to demo how multiple function files work,
we can create the file func/not_cool.sh
:
#!/bin/bash
not_cool() {
echo "NOT cool"
}
As mentioned, the filenames can be whatever you want but personally I follow the convention of adding the .sh extension and name the files the same as the function they contain. The shbang (
#!/bin/bash
), has no effect, but it makes it clear for both me and my text-editor that it is a bash file. The shbang in the function files will not be included in _MyScript
Lastly to test the functions we add some logic to
main()
in MyScript
so the file looks like this:
#!/bin/bash
main(){
if ((_o[cool-option1])); then
tellem
else
not_cool
fi
}
__dir=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") #bashbud
source "$__dir/_init.sh" #bashbud
$ ./MyScript --cool-option1
We're cool
$ ./MyScript
NOT cool
As you can see it all works without the need to "rebuild"
anything using make
, but to be able to get the same
functionality from ./_MyScript
we need to make
.
There are two main reasons for this:
- shellcheck
- distribution
[shellcheck] is a very good static code analyzer
for shell-scripts, but it is not excellent to analyze
shell-scripts that spans multiple files (using source
).
So by having all files concatenated as we do with
_MyScript
we can analyze that file with shellcheck
to get the correct feedback.
We can test this by changing the the following line
in func/not_cool.sh
echo "NOT cool"
to
echo "NOT cool" ${_o[cool-option1]:-not set}
and run: make && shellcheck _MyScript
This will update _MyScript
, pass it to shellcheck,
and shellcheck will tell us we should add quotes around the
variable.
The drawback here, is that will print what line
in _MyScript
where the error occurred, and not
the line in func/not_cool.sh
, but it is usually
easy to figure out which file the error stems from.
the template "watch" add a script that watches the files in the directory and automatically
shellcheck
when a change occur.
The second reason for having the concatenated version of the script is distribution. It is easier and more convenient to share and install a single file.
In the default template makefile there are
install: and uninstall:
targets, however they are
declared in config.mak
since they are things that
quite often needs to be fine tuned for the project.
If you trigger the default make install
target,
it will try to install the concatenated script
(_MyScript
) as MyScript in
DEST_DIR/PREFIX/bin.
Hardcoded in the GNUmakefile are: install-dev: and uninstall-dev:
targets. If you trigger make install-dev
a symlink to MyScript
(originally main.sh) is installed instead instead.
I prefer to use install-dev and use ~
as the prefix.
$ make PREFIX=~ install-dev
ln -s /home/bud/tmp/MyScript/MyScript /home/bud/bin/MyScript
The install target will also install manpage and LICENSE files if they exist.
A great side effect of using bashbud to manage a project
is that it becomes easy to keep documentation in sync.
I think most would agree that using the same table displaying
options as --help
in a manpage, webpage, README.md, wiki, e.t.c,
is nice. But how they are formatted and generated is
out of scoop for this wiki and the bashbud utility.
In the default templates config.mak
there are
manpage: and README.md
targets, manpage:
requires
go-md2man
, but they are there as starting points
or examples for how to do this. All bash projects
at budlabs uses bashbud, and many of them do this
stuff differently. Examine the content of the .cache
directory some files there are particularly useful
to include in documentation.
As we have seen quite a lot of files are automatically
generated. It is rarely desired to include auto generated
files in something like a git(1)
repository. Partly
because it is annoying and difficult to keep track
of and commit changes done to those files, and secondly
they are not "needed", since they are all generated
from existing documents with make
. This is why
all automatically generated documents that are not
in the .cache/
directory, are prefixed with an _
.
It makes it easy to ignore the files in .gitignore
or similar. And since git
is so common now,
a simple .gitignore
is included:
.cache/
**/_*
Related is also, that make clean
removes all
auto generated files.
As you have seen all configuration for make
has
been done by editing the config.mak
(or directly changing
variables on the command line make PREFIX=~ install-dev
).
config.mak
is included by the Makefile
and you can
add your own targets in config.mak
(manpage:
, README.md
).
The GNUmakefile will include any files with .mak
extension,
so you could also create a new file (custom.mak
).
The takeaway is that, you should hopefully never need
to edit the main Makefile, but in some cases you might
have to (f.i. if you need to install additional files, like
icons or .desktop files), in such case feel free
to do that, it is after all, just a Makefile.
Related is how the DEFAULT_GOAL in the makefile is setup:
.PHONY: clean all install-dev uninstall-dev
.DEFAULT_GOAL := all
all: $(CUSTOM_TARGETS) $(MONOLITH) $(MANPAGE_OUT) $(BASE)
Notice $(CUSTOM_TARGETS)
. This is a variable
that can be set in config.mak to have your own custom
targets included with the DEFAULT_GOAL.
Example: if you add the line CUSTOM_TARGETS += manpage
,
to config.mak
, whenever you execute make
for that
project it will not only generate the script but also
the manpage. Without manpage in CUSTOM_TARGETS, you
would need to do make manpage
separately.
One feature of the bashbud GNUmakefile is that it can
generate a file called func/_awklib.sh
. This is done
if there exist any files in the directory awklib
.
To demonstrate how and why, lets create awklib/main.awk
:
/^#/ {print; comments++}
and awklib/END.awk
END {print FILENAME " contained " comments " comments!"}
Then execute make
, to see that func/_awklib.sh
is created. This is how it will looks like:
#!/bin/bash
### _awklib() function is automatically generated
### from makefile based on the content of the ./awklib/ directory
_awklib() {
[[ -d $__dir ]] && { cat "$__dir/awklib/"* ; return ;} #bashbud
cat << 'EOAWK'
END {print FILENAME " contained " comments " comments!"}
/^#/ {print; comments++}
EOAWK
}
It gives us the function _awklib
which simply will
cat the contents of the files in awklib
. And we can
test it by adding this line to main()
in MyScript
:
awk -f <(_awklib) "$(readlink -f "${BASH_SOURCE[0]}")"
$ ./MyScrip
NOT cool not set
#!/bin/bash
/home/bud/tmp/MyScript/MyScript contained 1 comments!
The first line of output, is from not_cool()
,
but the two last lines are from awk, and we can see
that it only found 1 comment (the shbang) in the source
file.
The above example AWK is of course useless, but this is quite convenient if you have somewhat complex multiline AWK scripts. It keeps both the bash and the AWK cleaner and easier to maintain.
There exist a similar function for config files. Create the following files and directories:
conf/README.md
# this is a sample readme, cool funk!
> just to demonstrate how _createconf() works
conf/dotfiles/settings
# this is a fake settings file, that does nothing
VAR1="or is it?"
Now execute make
and func/_createconf.sh
should get
created, and it looks even messier than _awklib.sh
:
#!/bin/bash
### _createconf() function is automatically generated
### from makefile based on the content of the ./conf/ directory
_createconf() {
local trgdir="$1"
mkdir -p "$trgdir" "$trgdir"/dotfiles
if [[ -d $__dir ]]; then #bashbud
cat "$__dir/conf/README.md" > "$trgdir/README.md" #bashbud
else #bashbud
cat << 'EOCONF' > "$trgdir/README.md"
# this is a sample readme, cool funk!
> just to demonstrate how _createconf() works
EOCONF
fi #bashbud
if [[ -d $__dir ]]; then #bashbud
cat "$__dir/conf/dotfiles/settings" > "$trgdir/dotfiles/settings" #bashbud
else #bashbud
cat << 'EOCONF' > "$trgdir/dotfiles/settings"
# this is a fake settings file, that does nothing
VAR1="or is it?"
EOCONF
fi #bashbud
}
This gives you the function _createconf
which takes
a directory as its single argument. It will create
that directory, and all sub-directories needed to mirror
the layout defined in conf/
it will proceed creating
the files. Note that it does not copy the files instead they
are embedded in the script. So still you have a single
script file (_MyScript
), that will replicate conf/
if you ask it to.
To demonstrate, add the following line as the first
one in main()
:
[[ -d ~/.config/MyScript ]] || _createconf ~/.config/MyScript
It might be desirable to create a personal (or shared)
library of functions, that can be reused by other scripts.
It is easy to do so with bashbud. All directories
in ~/.config/bashbud
are templates, up to this point
we have only used the default template. But there
are more available, and its easy to create your own.
Lets look at the ERR
template:
~/.config/bashbud/ERR/
func/
ERR.sh
~/.config/bashbud/ERR/func/ERR.sh
#!/bin/bash
set -E
trap '(($? == 98)) && exit 98' ERR
ERX() { >&2 echo "[ERROR] $*" ; exit 98 ;}
ERR() { >&2 echo "[WARNING] $*" ;}
ERM() { >&2 echo "$*" ;}
The ERR
template contains a single directory, func/
,
which in turn contains a single file ERR.sh
.
If we now execute bashbud --template ERR
in our
MyScript/
directory, you will see that it copies
ERR.sh
file from the template into our func/
directory.
The easiest way to describe this is that templates,
will get merged in to the current tree. The files
copied over will not overwrite existing newer files
with the same name.
So if we wanted to create our own template we could
just create a new directory under ~/.config/bashbud
and add whatever file structure we wanted.
So lets try that by creating the following files and directories:
mkdir -p ~/.config/bashbud/budlabs/info \
~/.config/bashbud/budlabs/func
~/.config/bashbud/budlabs/info/budlabs.txt
This is just a sample text file
~/.config/bashbud/budlabs/func/budlabs.sh
#!/bin/bash
budlabs(){
hello "$1" welcome to budlabs!
}
And now: bashbud --template budlabs
, will add
copies of the files in our budlabs template.
Note that files imported this way are just copies, and you can modify them without worrying it will mess up the template files. And remember the default layout was the one that imported the Makefile, this is why it is no problem to modify it. And since template files doesn't overwrite files *, it will not cause any issues if you by accident try to import, say the default, template again.
bashbud --template TEMPLATE --pull
will have the same effect as without --pull , except it will update files in current directory that is older than the ones in the template directory. Adding--force
will always overwrite however some files are never overwritten (options, config.mak main.sh)
bashbud --template TEMPLATE --push
will update a template, this is basically the same action
as the previous, except it will copy files from the current
directory to the template directory in ~/.config/bashbud
.
So if we add a comment to the last line of func/bashbud.sh
, and
line of text to info/bashbud.txt
and execute:
bashbud --template budlabs --push
while we are
in the root of MyScript/
, it should update the budlabs template.
It is also possible to create templates,
(or add files to an existing template),
with the --add
option:
$ bashbud --template cool --add func/tellem.sh func/not_cool.sh
bashbud: creating new template: /home/bud/.config/bashbud/cool