Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
budRich edited this pageJun 13, 2022 ·14 revisions

tutorial

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

options file and _init.sh

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

auto include functions

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.

NB

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 inconfig.mak

Now, executemake again, and the content of_init.shand_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.

why is there two versions of MyScript

There are two main reasons for this:

  1. shellcheck
  2. 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"
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 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 automaticallyshellcheck 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.

installing scripts

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

generate manpage and documentation

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.cachedirectory some files there are particularly usefulto include in documentation.

whatabout git?

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.gitignoreor similar. And sincegit is so common now,a simple.gitignore is included:

.cache/**/_*

Related is also, thatmake clean removes allauto generated files.

extending the Makefile

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.

the bash and the AWK

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

embedded config files

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

customizing templates

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

[8]ページ先頭

©2009-2025 Movatter.jp