- Notifications
You must be signed in to change notification settings - Fork3
Home
If the commandbashbud
is executed without any arguments,an error message is printed:
$ bashbud[ERROR] --template'' not found.usage: bashbud [--template TEMPLATE] [TARGET_DIR]available templatesin~/.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 commandbashbud --template default MyScript
will create a directory calledMyScript, andthe files from~/.config/bashbud/default
will getcopied into this newMyScript/
directory.main.sh
will get renamed toMyScript
.And lastmake
will execute theMakefile.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 ashort embedded AWK scriptthat parses the content of theoptions 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 namesseparated by spaces. ( help version
).
_init.sh
contains two functions (__print_version
, and__print_help
),together with a generatedgetopt
loop, and lastly a call to:main "$@"
.
If options are added, removed or changed intheoptions file, it will be reflected inthis file.
As a test, we can add the following line tooptions:--cool-option1 --option-with-arg ROCKNROLL
If we now executemake
in theMyScriptdirectory,__print_help
and thegetopt
loopin_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 descriptionEOB}declare -A _ooptions=$(getopt \ --name"[ERROR]:MyScript" \ --options"v,h" \ --longoptions"cool-option1,version,help,option-with-arg:" --"$@")||exit 98evalset --"$options"unset optionswhiletrue;docase"$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 ;;esacshiftdone
Notice that the description for both options is:"short description". This text is taken from thecorresponding files indocs/options/
. To test, we canchange the content ofdocs/options/cool-option1
to:
if set all will be cool.
Also take note that thegetopt 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 theglobal_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 thevariableOPTIONS_ARRAY_NAME in
config.mak
If we change--options-with-arg ROCKNROLL
to--options-with-arg|-o
(removing the argument, adding short option)
in theoptions file. And executemake
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 descriptionEOB}declare -A _ooptions=$(getopt \ --name"[ERROR]:MyScript" \ --options"v,h,o" \ --longoptions"cool-option1,version,help,options-with-arg" --"$@")||exit 98evalset --"$options"unset optionswhiletrue;docase"$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 ;;esacshiftdone
Now lets remove--option-with-arg
completely fromtheoption file and executemake
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 exitEOB}declare -A _ooptions=$(getopt \ --name"[ERROR]:MyScript" \ --options"v,h" \ --longoptions"cool-option1,version,help" --"$@")||exit 98evalset --"$options"unset optionswhiletrue;docase"$1"in --help | -h ) __print_help&&exit ;; --version | -v ) __print_version&&exit ;; --cool-option1 ) _o[cool-option1]=1 ;; -- )shift;break ;;* )break ;;esacshiftdone
As expected, the references to--option-with-arg
isremoved from_init.sh
. But the filesdocs/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 rewritethe documentation.
The last two lines inMyScript
are important:
__dir=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") #bashbudsource "$__dir/_init.sh" #bashbud
First line sets the variable__dir
to the directoryof the script (resolved symlinks). Next line, simplysource
the_init.sh
file we looked at in the sectionabove.
The
__dir
variable is intended for internalbashbud
useonly, do never rely on it in your own functions, and donever overwrite or unset it.
remember the last line in_init.sh
is just a calltomain "$@"
. Themain()
is located inMyScript
.That's how the script works, you execute,./MyScript
, whichin turnsource
_init.sh
and parses the command-linewithgetopt
and lastly callmain
inMyScript
forthe actual execution of the script. With the exceptionif either--version
or--help
are set, in that case__print_version
or__print_help
will be called andthe script terminated. Or ifgetopt
see incorrectoptions passed, in which case an error message will getprinted.
If we look at_MyScript
, we will see that itbasically is the two filesMyScript
and_init.sh
neatly concatenated. But the last twolines fromMyScript
, mentioned above are notpresent. This is because they end with thecomment#bashbud
. Lines ending like that willnever be included in the concatenated version(_MyScript
) of the script.
If the first line of a fileends with '#bashbud' that whole filewill be ignored and not included in _MyScript
And as it is now, there is no difference from a usersperspective to execute,MyScript
vs_MyScript
.
Lets create a new directory and a new file:
mkdir funcecho '# tell em!' > func/tellem.sh
The name of the file can be anything, it doesn'tmatter, but the content must be valid bash. To startWe just add a comment to the file.
the name of the directory "func" however, mustbe "func". But you can change it by setting thevariableFUNCS_DIR in
config.mak
Now, executemake
again, and the content of_init.sh
and_MyScript
will be slightly different.
In_init.sh
just before thegetopt loop, the followinglines are added:
for ___f in "$__dir/func"/*; do . "$___f" ; done ; unset -v ___f
source (.
) each file infunc/
(currently only ourjust createdtellem.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 tofunc/
and they will beautomatically 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 infunc/tellem.sh
:
#!/bin/bashtellem() { echo "We're cool"}
And just to demo how multiple function files work,we can create the filefunc/not_cool.sh
:
#!/bin/bashnot_cool() { echo "NOT cool"}
As mentioned, the filenames can be whatever you wantbut personally I follow the convention of adding the.sh extension and name the files the same as thefunction they contain. Theshbang (
#!/bin/bash
),has no effect, but it makes it clear for bothme and my text-editor that it is a bash file. Theshbang in the function files will not be includedin _MyScript
Lastly to test the functions we add some logic tomain()
inMyScript
so the file looks like this:
#!/bin/bashmain(){ if ((_o[cool-option1])); then tellem else not_cool fi}__dir=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") #bashbudsource "$__dir/_init.sh" #bashbud
$ ./MyScript --cool-option1We're cool$ ./MyScriptNOT cool
As you can see it all works without the need to "rebuild"anything usingmake
, but to be able to get the samefunctionality from./_MyScript
we need tomake
.
There are two main reasons for this:
- shellcheck
- distribution
[shellcheck] is a very good static code analyzerfor shell-scripts, but it is not excellent to analyzeshell-scripts that spans multiple files (usingsource
).So by having all files concatenated as we do with_MyScript
we can analyze that file with shellcheckto get the correct feedback.
We can test this by changing the the following lineinfunc/not_cool.sh
echo "NOT cool"
toecho "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 thevariable.
The drawback here, is that will print what linein_MyScript
where the error occurred, and notthe line infunc/not_cool.sh
, but it is usuallyeasy to figure out which file the error stems from.
the template "watch" add a script that watchesthe files in the directory and automatically
shellcheck
when a change occur.
The second reason for having the concatenatedversion of the script is distribution. It iseasier and more convenient to share and install asingle file.
In thedefault template makefile there areinstall: and uninstall:
targets, however they aredeclared inconfig.mak
since they are things thatquite often needs to be fine tuned for the project.
If you trigger the defaultmake install
target,it will try to install the concatenated script(_MyScript
) asMyScript inDEST_DIR/PREFIX/bin.
Hardcoded in the GNUmakefile are:install-dev: and uninstall-dev:
targets. If you triggermake install-dev
a symlink toMyScript
(originally main.sh) is installed instead instead.
I prefer to useinstall-dev and use~
as the prefix.
$ make PREFIX=~ install-devln -s /home/bud/tmp/MyScript/MyScript /home/bud/bin/MyScript
Theinstall target will also installmanpageandLICENSE files if they exist.
A great side effect of using bashbud to manage a projectis that it becomes easy to keep documentation in sync.I think most would agree that using the same table displayingoptions as--help
in a manpage, webpage, README.md, wiki, e.t.c,is nice. But how they are formatted and generated isout of scoop for this wiki and the bashbud utility.
In thedefault templatesconfig.mak
there aremanpage: and README.md
targets,manpage:
requiresgo-md2man
, but they are there as starting pointsor examples for how to do this. All bash projectsatbudlabs uses bashbud, and many of them do thisstuff differently. Examine the content of the.cache
directory some files there are particularly usefulto include in documentation.
As we have seen quite a lot of files are automaticallygenerated. It is rarely desired to include auto generatedfiles in something like agit(1)
repository. Partlybecause it is annoying and difficult to keep trackof and commit changes done to those files, and secondlythey are not "needed", since they are all generatedfrom existing documents withmake
. This is whyall automatically generated documents that are notin the.cache/
directory, are prefixed with an_
.It makes it easy to ignore the files in.gitignore
or similar. And sincegit
is so common now,a simple.gitignore
is included:
.cache/**/_*
Related is also, thatmake clean
removes allauto generated files.
As you have seen all configuration formake
hasbeen done by editing theconfig.mak
(or directly changingvariables on the command linemake PREFIX=~ install-dev
).config.mak
is included by theMakefile
and you canadd your own targets inconfig.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 needto edit the main Makefile, but in some cases you mighthave to (f.i. if you need to install additional files, likeicons or .desktop files), in such case feel freeto do that, it is after all, just a Makefile.
Related is how theDEFAULT_GOAL in the makefileis setup:
.PHONY: clean all install-dev uninstall-dev.DEFAULT_GOAL := allall: $(CUSTOM_TARGETS) $(MONOLITH) $(MANPAGE_OUT) $(BASE)
Notice$(CUSTOM_TARGETS)
. This is a variablethat can be set in config.mak to have your own customtargets included with the DEFAULT_GOAL.
Example: if you add the lineCUSTOM_TARGETS += manpage
,toconfig.mak
, whenever you executemake
for thatproject it will not only generate the script but alsothe manpage. Without manpage inCUSTOM_TARGETS, youwould need to domake manpage
separately.
One feature of the bashbud GNUmakefile is that it cangenerate a file calledfunc/_awklib.sh
. This is doneif there exist any files in the directoryawklib
.To demonstrate how and why, lets createawklib/main.awk
:
/^#/ {print; comments++}
andawklib/END.awk
END {printFILENAME" contained" comments" comments!"}
Then executemake
, to see thatfunc/_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;}#bashbudcat<< 'EOAWK'END {print FILENAME " contained " comments " comments!"}/^#/ {print; comments++}EOAWK}
It gives us the function_awklib
which simply willcat the contents of the files inawklib
. And we cantest it by adding this line tomain()
inMyScript
:awk -f <(_awklib) "$(readlink -f "${BASH_SOURCE[0]}")"
$ ./MyScripNOT cool notset#!/bin/bash/home/bud/tmp/MyScript/MyScript contained 1 comments!
The first line of output, is fromnot_cool()
,but the two last lines are from awk, and we can seethat it only found 1 comment (the shbang) in the sourcefile.
The above example AWK is of course useless, but thisis quite convenient if you have somewhat complex multilineAWK scripts. It keeps both the bash and the AWK cleanerand 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 nothingVAR1="or is it?"
Now executemake
andfunc/_createconf.sh
should getcreated, 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"/dotfilesif [[-d$__dir ]];then#bashbudcat"$__dir/conf/README.md">"$trgdir/README.md"#bashbudelse#bashbudcat<< 'EOCONF' > "$trgdir/README.md"# this is a sample readme, cool funk!> just to demonstrate how _createconf() worksEOCONFfi#bashbudif [[-d$__dir ]];then#bashbudcat"$__dir/conf/dotfiles/settings">"$trgdir/dotfiles/settings"#bashbudelse#bashbudcat<< 'EOCONF' > "$trgdir/dotfiles/settings"# this is a fake settings file, that does nothingVAR1="or is it?"EOCONFfi#bashbud}
This gives you the function_createconf
which takesa directory as its single argument. It will createthat directory, and all sub-directories needed to mirrorthe layout defined inconf/
it will proceed creatingthe files. Note that it does not copy the files instead theyare embedded in the script. So still you have a singlescript file (_MyScript
), that will replicateconf/
if you ask it to.
To demonstrate, add the following line as the firstone inmain()
:
[[ -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 withbashbud. All directoriesin~/.config/bashbud
aretemplates, up to this pointwe have only used thedefault template. But thereare more available, and its easy to create your own.
Lets look at theERR
template:
~/.config/bashbud/ERR/ func/ ERR.sh
~/.config/bashbud/ERR/func/ERR.sh
#!/bin/bashset -Etrap'(($? == 98)) && exit 98' ERRERX() {>&2echo"[ERROR]$*";exit 98;}ERR() {>&2echo"[WARNING]$*";}ERM() {>&2echo"$*";}
TheERR
template contains a single directory,func/
,which in turn contains a single fileERR.sh
.
If we now executebashbud --template ERR
in ourMyScript/
directory, you will see that it copiesERR.sh
file from the template into ourfunc/
directory.The easiest way to describe this is that templates,will getmerged in to the current tree. The filescopied over will not overwrite existingnewer fileswith the same name.
So if we wanted to create our own template we couldjust create a new directory under~/.config/bashbud
and add whatever file structure we wanted.
So lets try that by creating the following filesand 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/bashbudlabs(){ hello"$1" welcome to budlabs!}
And now:bashbud --template budlabs
, will addcopies of the files in our budlabs template.
Note that files imported this way are just copies,and you can modify them without worrying it willmess up the template files. And remember the defaultlayout was the one that imported the Makefile, this iswhy it is no problem to modify it. And since templatefiles doesn't overwrite files *, it willnot cause any issues if you by accident tryto import, say the default, template again.
bashbud --template TEMPLATE --pull
will have thesame effect as without --pull , except it will updatefiles in current directory that is older than the onesin the template directory. Adding--force
will always overwritehowever some files are never overwritten (options, config.mak main.sh)
bashbud --template TEMPLATE --push
will update a template, this is basically the same actionas the previous, except it will copy files from the currentdirectoryto the template directory in~/.config/bashbud
.
So if we add a comment to the last line offunc/bashbud.sh
, andline of text toinfo/bashbud.txt
and execute:
bashbud --template budlabs --push
while we arein the root ofMyScript/
, 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.shbashbud: creating new template: /home/bud/.config/bashbud/cool