├── .gitignore ├── README.md ├── code ├── bash_manager.sh ├── bash_manager_core-1.0.sh ├── bash_manager_core-1.01.sh ├── bash_manager_core-1.02.sh ├── bash_manager_core-1.03.sh ├── bash_manager_core-1.04.sh ├── bash_manager_core-1.05.sh ├── bash_manager_core-1.06.sh ├── bash_manager_core-1.07.sh ├── bash_manager_core-1.08.sh ├── bash_manager_core-1.09.sh ├── tutorials │ ├── hello-world │ │ ├── config.d │ │ │ └── hello.conf.txt │ │ ├── config.defaults │ │ └── tasks.d │ │ │ └── hello.sh │ └── the-flexible-software │ │ ├── config.d │ │ └── me.txt │ │ ├── config.defaults │ │ └── tasks.d │ │ ├── t1.sh │ │ ├── t2.sh │ │ ├── t3.sh │ │ ├── t4.sh │ │ └── t5.sh └── utils │ ├── new_app.sh │ └── new_home.sh ├── doc ├── aliases.eng.md ├── commandLineOptions.eng.md ├── constraints.eng.md ├── foreign-script-guidelines.eng.md ├── images │ └── bash-manager-structure.jpg ├── install-bashmanager.eng.md ├── task-author-cheatsheet.eng.md ├── task-author-guidelines.eng.md └── task-author-reserved-functions.eng.md └── the-flexible-software-tutorial.md /.gitignore: -------------------------------------------------------------------------------- 1 | private/ 2 | plugins/ 3 | .idea/ -------------------------------------------------------------------------------- /code/bash_manager.sh: -------------------------------------------------------------------------------- 1 | bash_manager_core-1.09.sh -------------------------------------------------------------------------------- /code/bash_manager_core-1.0.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | 5 | 6 | 7 | ############################################################ 8 | # BASH MANAGER 1.01 - 2015-09-10 9 | # By LingTalfi 10 | ############################################################ 11 | major=${BASH_VERSION:0:1} 12 | if [ $major -lt 4 ]; then 13 | echo "This programs requires a minimum verson of bash 4 (your version is $BASH_VERSION)" 14 | exit 1 15 | fi 16 | 17 | ############################################################ 18 | # ERROR POLICY IN A NUTSHELL 19 | ############################################################ 20 | # 21 | # In short, my advice for task creators: should you use error or warning? 22 | # -> Use error if something that the maintainer wouldn't expect occurs 23 | # -> User warning for yourself while debugging your own task 24 | # 25 | # 26 | # The script (and probably any script by the way) has three possible issues: 27 | # 28 | # - success 29 | # - failure 30 | # - sucess with recoverable failures 31 | # 32 | # In case of success, there is not much to say. 33 | # In case of failure, the error message is sent to STDERR. 34 | # In case of recoverable failures, the message is also sent to 35 | # STDERR (because the admin should be able to see it), but 36 | # we also ask this question: 37 | # should we continue the script anyway or exit? 38 | # The answer depends on the user, so we provide the 39 | # STRICT_MODE variable for that, which can take one of 40 | # two values: 41 | # 0 (default): the script will always try to continue 42 | # if possible. 43 | # 1: the script exits when the first error is encountered. 44 | # 45 | # Failure and recoverable failures are handled by the error function. 46 | # 47 | # At our disposal, we have the following functions: 48 | # - log (outputs to STDOUT, only if VERBOSE=1) 49 | # - warning (outputs to STDOUT in orange) 50 | # - error (failure, recoverable failure) 51 | # 52 | # The log function displays a message only if VERBOSE=1. 53 | # By default, VERBOSE=0. 54 | # The user can set VERBOSE=1 by using the -v option on the command line. 55 | # 56 | # The warning is like the log, but the text is orange. 57 | # However, the warning always shows up, without respect to the VERBOSE option. 58 | # 59 | # The error method sends a message to STDERR. 60 | # Also, if STRICT_MODE=1, the script will stop immediately. 61 | # By default, STRICT_MODE=0. 62 | # The user can set STRICT_MODE=0 by using the -s option on the command line. 63 | # 64 | # 65 | ############################################################ 66 | 67 | ############################################################ 68 | # internal variables, please do not modify 69 | ############################################################ 70 | RETURN_CODE=0 71 | ERROR_CODE=1 72 | VERBOSE=0 # make the log function output something 73 | STRICT_MODE=0 # if 1, the script exits when an error is triggered 74 | CONFIG_FILES=() 75 | TASKS_LIST=() 76 | TASKS_LIST_OPTIONS=() 77 | TASKS_NAMES=() # per config file 78 | PROJECTS_LIST=() 79 | PROJECTS_LIST_OPTIONS=() 80 | PROJECTS_NAMES=() # per config file 81 | declare -A _CONFIG # this contains the values from config.defaults, should never be touched 82 | declare -A CONFIG # every time a project is called, the CONFIG array is recreated: it's a copy of _CONFIG. 83 | # tasks can then update the CONFIG array in the scope of one project. 84 | # For instance, it is used by the depositories.sh task in order to allow the user to 85 | # choose a depository per project from the config file. 86 | declare -A VALUES # concerned task's values 87 | declare -A ALL_VALUES # other tasks' values, namespaced with the format taskName_key (instead of key) 88 | declare -A OTHER_VALUES # other project's values 89 | declare -A CONFIG_OPTIONS # config options set via the command line options 90 | 91 | 92 | COLOR_TASK='\033[1m' 93 | COLOR_IMPORTANT='\033[0;44;33m' 94 | COLOR_WARNING='\033[0;31m' 95 | COLOR_STOP='\033[0m' 96 | 97 | COL_IMPORTANT=$(echo -e "${COLOR_IMPORTANT}") 98 | COL_WARNING=$(echo -e "${COLOR_WARNING}") 99 | COL_STOP=$(echo -e "${COLOR_STOP}") 100 | 101 | 102 | 103 | #---------------------------------------- 104 | # config.defaults (and command line) specials 105 | # They all begin with an underscore 106 | #---------------------------------------- 107 | _program_name="bash manager" 108 | 109 | 110 | 111 | 112 | ############################################################ 113 | # Functions 114 | ############################################################ 115 | 116 | 117 | #---------------------------------------- 118 | # Use this function to split a string with delimiters. 119 | #---------------------------------------- 120 | # Splits the string with the given delimiter. 121 | # The results are put in the SPLITLINE_ARR array. 122 | # The delimiter is a string of length 1. 123 | # 124 | # There are two different cases: 125 | # 126 | # - if the delimiter is the first char of string: 127 | # then the keys of the SPLITLINE_ARR will 128 | # be those passed to the function (argNameN, ...) 129 | # and the function returns 1. 130 | # 131 | # - if the delimiter is not the first char of string, 132 | # the SPLITLINE_ARR is empty and the function 133 | # returns 0. 134 | # 135 | 136 | declare -A SPLITLINE_ARR 137 | splitLine () # ( string, delimiter [, argNameN ]* ) 138 | { 139 | unset SPLITLINE_ARR 140 | declare -gA SPLITLINE_ARR=[] 141 | string="$1" 142 | delimiter="$2" 143 | if [ "$delimiter" = "${string:0:1}" ]; then 144 | string="${string:1}" 145 | shift 146 | shift 147 | key="$1" 148 | local i 149 | i=1 150 | while [ -n "$key" ]; do 151 | key="$1" 152 | if [ -n "$key" ]; then 153 | value=$(echo "$string" | cut -d"${delimiter}" -f${i} | xargs) 154 | SPLITLINE_ARR["$key"]="$value" 155 | (( i++ )) 156 | shift 157 | fi 158 | done 159 | return 1; 160 | 161 | else 162 | SPLITLINE_ARR["_default"]="$string" 163 | return 0; 164 | fi 165 | 166 | } 167 | 168 | 169 | 170 | 171 | 172 | # Utils 173 | error () # ( message ) 174 | { 175 | echo -e "$_program_name: error: $1" >&2 176 | RETURN_CODE=$ERROR_CODE 177 | 178 | if [ 1 -eq $VERBOSE ]; then 179 | printTrace 180 | fi 181 | 182 | if [ 1 -eq $STRICT_MODE ]; then 183 | exit 1 184 | fi 185 | } 186 | 187 | 188 | 189 | warning () # ( message) 190 | { 191 | old=$VERBOSE 192 | VERBOSE=1 193 | log "$1" "1" 194 | VERBOSE=$old 195 | } 196 | 197 | confError () # () 198 | { 199 | error "Bad config: $1" 200 | } 201 | 202 | abort (){ 203 | error "Aborting..." 204 | exit "$ERROR_CODE" 205 | } 206 | 207 | 208 | # level=0 means no special color 209 | # level=1 means warning color 210 | # level=2 means important color 211 | log () # (message, level=0) 212 | { 213 | if [ 1 -eq $VERBOSE ]; then 214 | if [ -z "$2" ]; then 215 | echo -e "$1" | sed "s/^/$_program_name\(v\): /g" 216 | else 217 | if [ "1" = "$2" ]; then 218 | echo -e "$1" | sed -e "s/^.*$/${_program_name}\(v\): ${COL_WARNING}&${COL_STOP}/g" 219 | elif [ "2" = "$2" ]; then 220 | echo -e "$1" | sed -e "s/^.*$/${_program_name}\(v\): ${COL_IMPORTANT}&${COL_STOP}/g" 221 | fi 222 | fi 223 | fi 224 | } 225 | 226 | 227 | 228 | 229 | # outputs a list of elements of separated by a sep 230 | toList ()# ( arrayEls, ?sep ) 231 | { 232 | sep="${2:-, }" 233 | arr=("${!1}") 234 | i=0 235 | for path in "${arr[@]}"; do 236 | if [ $i -eq 1 ]; then 237 | echo -n "$sep" 238 | fi 239 | echo -n "$path" 240 | i=1 241 | done 242 | echo 243 | } 244 | 245 | 246 | # same as toList, but prints a header first 247 | printList ()# ( header, arrayEls, ?sep=", " ) 248 | { 249 | echo -n "$1" 250 | sep=${3:-, } 251 | toList "$2" "$sep" 252 | } 253 | 254 | printAssocArray ()# ( assocArrayName ) 255 | { 256 | var=$(declare -p "$1") 257 | eval "declare -A _arr="${var#*=} 258 | for k in "${!_arr[@]}"; do 259 | echo "$k: ${_arr[$k]}" 260 | done 261 | 262 | } 263 | 264 | # outputs an array as a stack beginning by a leading expression 265 | toStack ()# ( arrayEls, ?leader="-- ") 266 | { 267 | lead="${2:--- }" 268 | arr=("${!1}") 269 | echo 270 | for path in "${arr[@]}"; do 271 | echo "$lead$path" 272 | done 273 | } 274 | 275 | 276 | # same as toStack, but prints a header first 277 | printStack ()# ( header, arrayEls, ?leader="-- " ) 278 | { 279 | echo -n "$1" 280 | lead=${3:--- } 281 | toStack "$2" "$lead" 282 | } 283 | 284 | printStackOrList () 285 | { 286 | name=("${!2}") 287 | len="${#name[@]}" 288 | if [ $len -gt 2 ]; then 289 | printStack "$1" "$2" 290 | else 291 | third=${3:-, } 292 | printList "$1" "$2" 293 | fi 294 | } 295 | 296 | # This method should print ---non blank and comments stripped--- lines of the given config file 297 | printConfigLines () 298 | { 299 | while read line || [ -n "$line" ]; do 300 | 301 | # strip comments 302 | line=$(echo "$line" | cut -d# -f1 ) 303 | if [ -n "$line" ]; then 304 | echo "$line" 305 | fi 306 | done < "$1" 307 | } 308 | 309 | # Store the key and values found in configFile into the array which arrayEls is given 310 | collectConfig ()#( arrayName, configFile ) 311 | { 312 | arr=("$1") 313 | while read line 314 | do 315 | key=$(echo "$line" | cut -d= -f1 ) 316 | value=$(echo "$line" | cut -d= -f2- ) 317 | if [ ${#value} -gt 0 ]; then 318 | arr["$key"]="$value" 319 | fi 320 | done < <(printConfigLines "$2") 321 | } 322 | 323 | 324 | 325 | dumpAssoc ()# ( arrayName ) 326 | { 327 | title="${1^^}" 328 | echo 329 | echo "======= $title ========" 330 | printAssocArray "$1" 331 | echo "======================" 332 | echo 333 | } 334 | 335 | 336 | parseAllValues ()# ( configFile ) 337 | { 338 | configFile="$1" 339 | namespace="" 340 | lineno=1 341 | while read line || [ -n "$line" ]; do 342 | if [ -n "$line" ]; then 343 | line="$(echo $line | xargs)" # trimming 344 | 345 | # strip comments: lines which first char is a sharp (#) 346 | if ! [ '#' = "${line:0:1}" ]; then 347 | if [[ "$line" == *"="* ]]; then 348 | if [ -n "$namespace" ]; then 349 | key=$(echo "$line" | cut -d= -f1 ) 350 | value=$(echo "$line" | cut -d= -f2- ) 351 | ALL_VALUES["${namespace}_${key}"]="$value" 352 | 353 | inArray "$key" "${PROJECTS_NAMES[@]}" 354 | if [ 1 -eq $? ]; then 355 | PROJECTS_NAMES+=("$key") 356 | log "Project found: $key" 357 | fi 358 | 359 | else 360 | warning "No namespace found for the first lines of file $configFile" 361 | fi 362 | else 363 | # if the last char is colon (:) and the line doesn't contain an equal symbol(=) 364 | # then it defines a new namespace 365 | if [ ":" = "${line:${#line}-1:${#line}}" ]; then 366 | namespace="${line:0:${#line}-1}" 367 | log "Namespace found: $namespace" 368 | TASKS_NAMES+=("$namespace") 369 | else 370 | error "Unknown line type in file $configFile, line $lineno: ignoring" 371 | fi 372 | fi 373 | fi 374 | fi 375 | (( lineno++ )) 376 | done < "$configFile" 377 | } 378 | 379 | 380 | 381 | # We can use this function to do one of the following: 382 | # -- check if an associative array has a certain key inArray "myKey" "${!myArray[@]}" 383 | # -- check if an associative array contains a certain value inArray "myValue" "${myArray[@]}" 384 | inArray () # ( value, arrayKeysOrValues ) 385 | { 386 | local e 387 | for e in "${@:2}"; do 388 | [[ "$e" == "$1" ]] && return 0; 389 | done 390 | return 1 391 | } 392 | 393 | 394 | printTrace() # ( commandName?, exit=0? ) 395 | { 396 | 397 | m="" 398 | m+="Trace:\n" 399 | m+="----------------------\n" 400 | local frame=0 401 | last=0 402 | while [ 0 -eq $last ]; do 403 | line="$( caller $frame )" 404 | last=$? 405 | ((frame++)) 406 | if [ 0 -eq $last ]; then 407 | m+=$(echo "$line" | awk '{printf "function " $2 " in file " $3 ", line " $1 "\n" }') 408 | m+="\n" 409 | fi 410 | done 411 | echo -e "$m" 412 | } 413 | 414 | 415 | startTask () #( taskName ) 416 | { 417 | log "${COLOR_TASK}---- TASK: $1 ------------${COLOR_STOP}" 418 | } 419 | 420 | endTask ()#( taskName ) 421 | { 422 | log "${COLOR_TASK}---- ENDTASK: $1 ------------${COLOR_STOP}" 423 | } 424 | 425 | 426 | 427 | printDate () 428 | { 429 | echo $(date +"%Y-%m-%d__%H-%M") 430 | } 431 | 432 | # used by chronos scripts 433 | printCurrentTime () 434 | { 435 | echo $(date +"%Y-%m-%d %H:%M:%S") 436 | } 437 | 438 | 439 | 440 | # This function will export the CONFIG array for other scripting 441 | # languages, like php or python for instance. 442 | # variables are exported using the following format: 443 | # BASH_MANAGER_CONFIG_$KEY 444 | 445 | exportConfig ()# () 446 | { 447 | local KEY 448 | for key in "${!CONFIG[@]}"; do 449 | KEY=$(echo "$key" | tr '[:lower:]' '[:upper:]') 450 | export "BASH_MANAGER_CONFIG_${KEY}"="${CONFIG[$key]}" 451 | done 452 | } 453 | 454 | # This function work in pair with exportConfig. 455 | # What it does is process the output of a script coded in 456 | # another scripting language (php, python, perl...). 457 | # Such a script is called "foreign" script 458 | # There is a convention for those foreign scripts to be aware of: 459 | # - Every line should end with the carriage return 460 | # - a line starting with 461 | # log: 462 | # will be send to the bash manager log 463 | # 464 | # - foreign scripts can update the content of the CONFIG array. 465 | # a line with the following format: 466 | # BASH_MANAGER_CONFIG_$KEY=$VALUE 467 | 468 | # will add the key $KEY with value $VALUE to the 469 | # CONFIG array. 470 | # 471 | 472 | 473 | processScriptOutput () # ( vars ) 474 | { 475 | local isConf 476 | while read line 477 | do 478 | if [ "log:" = "${line:0:4}" ]; then 479 | log "${line:4}" 480 | elif [ "warning:" = "${line:0:8}" ]; then 481 | warning "${line:8}" 482 | elif [ "error:" = "${line:0:6}" ]; then 483 | error "${line:6}" 484 | elif [ "exit:" = "${line:0:5}" ]; then 485 | exit $(echo "$line" | cut -d: -f2) 486 | else 487 | isConf=0 488 | if [ "BASH_MANAGER_CONFIG_" = "${line:0:20}" ]; then 489 | if [[ "$line" == *"="* ]]; then 490 | value=$(echo "$line" | cut -d= -f2- ) 491 | key=$(echo "$line" | cut -d= -f1 ) 492 | key="${key:20}" 493 | CONFIG["$key"]="$value" 494 | isConf=1 495 | fi 496 | fi 497 | if [ 0 -eq $isConf ]; then 498 | echo "$line" 499 | fi 500 | fi 501 | done <<< "$1" 502 | } 503 | 504 | 505 | printRealTaskName () # ( taskString ) 506 | { 507 | echo "$1" | cut -d'(' -f1 508 | } 509 | 510 | printRealTaskExtension () # ( taskString ) 511 | { 512 | extension=sh 513 | ext=$(echo "$1" | cut -d'(' -s -f2) 514 | if [ -n "$ext" ]; then 515 | if [ ')' = "${ext:${#ext}-1:${#ext}}" ]; then 516 | echo "${ext:0:${#ext}-1}" 517 | return 0 518 | fi 519 | fi 520 | echo "$extension" 521 | } 522 | 523 | ############################################################ 524 | # MAIN SCRIPT 525 | ############################################################ 526 | 527 | 528 | #---------------------------------------- 529 | # Processing command line options 530 | #---------------------------------------- 531 | while :; do 532 | key="$1" 533 | case "$key" in 534 | --option-*=*) 535 | optionName=$(echo "$1" | cut -d= -f1) 536 | optionValue=$(echo "$1" | cut -d= -f2-) 537 | optionName="${optionName:9}" 538 | CONFIG_OPTIONS["$optionName"]="$optionValue" 539 | ;; 540 | -c) 541 | CONFIG_FILES+=("$2") 542 | shift 543 | ;; 544 | -h) 545 | _home=("$2") 546 | shift 547 | ;; 548 | -p) 549 | PROJECTS_LIST_OPTIONS+=("$2") 550 | shift 551 | ;; 552 | -s) 553 | STRICT_MODE=1 554 | ;; 555 | -t) 556 | TASKS_LIST_OPTIONS+=("$2") 557 | shift 558 | ;; 559 | -v) 560 | VERBOSE=1 561 | ;; 562 | *) 563 | break 564 | ;; 565 | esac 566 | shift 567 | done 568 | 569 | 570 | #---------------------------------------- 571 | # bash manager requires that the _home variable 572 | # is defined, either from the caller script, or with the command line options -h 573 | #---------------------------------------- 574 | if [ -z "$_home" ]; then 575 | error "variable _home not defined, you can set it via the -h option, or create a wrapper script which defines _home and sources this bash manager script" 576 | exit $ERROR_CODE 577 | fi 578 | cd "$_home" 579 | # resolve relative paths 580 | _home=$("pwd") 581 | 582 | 583 | #---------------------------------------- 584 | # Check the basic structure 585 | # - home path 586 | # ----- config.defaults 587 | # ----- config.d 588 | # ----- tasks.d 589 | #---------------------------------------- 590 | # Turning _home as an absolute path 591 | log "HOME is set to: $_home" 592 | 593 | configDefaults="$_home/config.defaults" 594 | configDir="$_home/config.d" 595 | tasksDir="$_home/tasks.d" 596 | 597 | if ! [ -f "$configDefaults" ]; then 598 | confError "Cannot find config.defaults file, check your _home variable (not found: $configDefaults)" 599 | 600 | elif ! [ -d "$configDir" ]; then 601 | confError "Cannot find config.d directory, check your _home variable (not found: $configDir)" 602 | elif ! [ -d "$tasksDir" ]; then 603 | confError "Cannot find tasks.d directory, check your _home variable (not found: $tasksDir)" 604 | fi 605 | [ $RETURN_CODE -eq 1 ] && abort 606 | 607 | 608 | 609 | 610 | 611 | #---------------------------------------- 612 | # Prepare _CONFIG from config.defaults 613 | #---------------------------------------- 614 | while read line 615 | do 616 | key=$(echo "$line" | cut -d= -f1 ) 617 | value=$(echo "$line" | cut -d= -f2- ) 618 | if [ ${#value} -gt 0 ]; then 619 | _CONFIG["$key"]="$value" 620 | fi 621 | done < <(printConfigLines "$configDefaults") 622 | 623 | 624 | for key in "${!CONFIG_OPTIONS[@]}"; do 625 | _CONFIG["$key"]="${CONFIG_OPTIONS[$key]}" 626 | done 627 | 628 | # we will add a special _HOME value for the tasks 629 | _CONFIG[_HOME]="$_home" 630 | 631 | 632 | 633 | 634 | 635 | #---------------------------------------- 636 | # Collecting configuration files. 637 | # If the user doesn't specify config files on the command line, 638 | # we use all the config files located in the HOME/config.d directory 639 | #---------------------------------------- 640 | cd "$configDir" 641 | if [ -z $CONFIG_FILES ]; then 642 | CONFIG_FILES=($(find . | grep '\.txt$')) 643 | else 644 | for i in "${!CONFIG_FILES[@]}"; do 645 | CONFIG_FILES[$i]="./${CONFIG_FILES[$i]}.txt" 646 | done 647 | fi 648 | 649 | 650 | 651 | #---------------------------------------- 652 | # Outputting some info on STDOUT 653 | #---------------------------------------- 654 | log "$(printStack 'Collecting config files: ' CONFIG_FILES[@])" 655 | if [ -z $TASKS_LIST_OPTIONS ]; then 656 | log "Collecting tasks: (all)" 657 | else 658 | log "$(printStack 'Collecting tasks: ' TASKS_LIST_OPTIONS[@])" 659 | fi 660 | if [ -z $PROJECTS_LIST_OPTIONS ]; then 661 | log "Collecting projects: (all)" 662 | else 663 | log "$(printStack 'Collecting projects: ' PROJECTS_LIST_OPTIONS[@])" 664 | fi 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | #---------------------------------------- 674 | # Spread special internal variables 675 | #---------------------------------------- 676 | if [ -n "${_CONFIG[_program_name]}" ]; then 677 | _program_name="${_CONFIG[_program_name]}" 678 | fi 679 | 680 | 681 | 682 | 683 | #---------------------------------------- 684 | # Processing all config files found, one after another. 685 | #---------------------------------------- 686 | cd "$configDir" 687 | for configFile in "${CONFIG_FILES[@]}"; do 688 | if [ -f "$configFile" ]; then 689 | log "Scanning config file $configFile" 690 | 691 | unset VALUES 692 | unset ALL_VALUES 693 | 694 | declare -A VALUES 695 | declare -A ALL_VALUES 696 | 697 | 698 | unset TASKS_NAMES 699 | TASKS_NAMES=() 700 | unset PROJECTS_NAMES 701 | PROJECTS_NAMES=() 702 | unset PROJECTS_LIST 703 | PROJECTS_LIST=() 704 | unset TASKS_LIST 705 | TASKS_LIST=() 706 | 707 | 708 | # For every config file, 709 | # we collect the following arrays: 710 | # - PROJECT_NAMES: all the projects found in the current config file (no duplicate), in order of appearance 711 | # - TASKS_NAME: all the tasks found in the current config file (no duplicate), in order of appearance 712 | # - ALL_VALUES, an array that contains all the values we found, and looks like this: 713 | # ALL_VALUES[task_project]=value 714 | # ALL_VALUES[task_project2]=value 715 | # ALL_VALUES[task2_project]=value 716 | # ... 717 | # ... 718 | # 719 | parseAllValues "$configFile" 720 | 721 | 722 | # Preparing TASKS_LIST 723 | # If no task is defined, 724 | # we use all tasks found in TASKS_NAME 725 | if [ 0 -eq ${#TASKS_LIST_OPTIONS[@]} ]; then 726 | for task in "${TASKS_NAMES[@]}"; do 727 | TASKS_LIST+=("$task") 728 | done 729 | else 730 | for task in "${TASKS_LIST_OPTIONS[@]}"; do 731 | TASKS_LIST+=("$task") 732 | done 733 | fi 734 | # dumpAssoc "TASKS_NAMES" 735 | # dumpAssoc "TASKS_LIST" 736 | 737 | 738 | # Preparing PROJECTS_LIST_OPTIONS 739 | # If no task is defined, 740 | # we use all tasks found in TASKS_NAME 741 | if [ 0 -eq ${#PROJECTS_LIST_OPTIONS[@]} ]; then 742 | for project in "${PROJECTS_NAMES[@]}"; do 743 | PROJECTS_LIST+=("$project") 744 | done 745 | else 746 | for project in "${PROJECTS_LIST_OPTIONS[@]}"; do 747 | PROJECTS_LIST+=("$project") 748 | done 749 | fi 750 | # dumpAssoc "PROJECTS_LIST" 751 | 752 | 753 | 754 | # processing the projects 755 | for project in "${PROJECTS_LIST[@]}"; do 756 | 757 | log "Processing project $project" "2" 758 | 759 | 760 | unset CONFIG 761 | unset OTHER_VALUES 762 | declare -A CONFIG 763 | declare -A OTHER_VALUES 764 | 765 | 766 | # creating a CONFIG copy for the tasks to use 767 | for key in "${!_CONFIG[@]}"; do 768 | CONFIG["$key"]="${_CONFIG[$key]}" 769 | done 770 | # dumpAssoc "CONFIG" 771 | 772 | 773 | # preparing other values for this project 774 | plen=${#project} 775 | (( plen++ )) # add the underscore length 776 | for key in "${!ALL_VALUES[@]}"; do 777 | if [ "_$project" = "${key:${#key}-$plen}" ]; then 778 | ptask="${key:0:${#key}-$plen}" 779 | OTHER_VALUES["$ptask"]="${ALL_VALUES[$key]}" 780 | fi 781 | done 782 | # dumpAssoc "OTHER_VALUES" 783 | 784 | 785 | 786 | for task in "${TASKS_LIST[@]}"; do 787 | 788 | # Tasks which name begins with underscore are skipped 789 | # This is handy for quick testing 790 | if ! [ "_" = "${task:0:1}" ]; then 791 | 792 | 793 | # handling foreign script direct call 794 | # notation is: 795 | # taskName(extension) 796 | # 797 | # 798 | realTaskExtension=$(printRealTaskExtension "$task") 799 | realTask=$(printRealTaskName "$task") 800 | 801 | 802 | 803 | 804 | 805 | taskScript="$tasksDir/$realTask.${realTaskExtension}" 806 | if [ -f "$taskScript" ]; then 807 | 808 | # Prepare the values to pass to the script 809 | 810 | key="${task}_${project}" 811 | 812 | inArray "$key" "${!ALL_VALUES[@]}" 813 | if [ 0 -eq $? ]; then 814 | VALUE="${ALL_VALUES[$key]}" 815 | 816 | 817 | CONFIG[_VALUE]="$VALUE" 818 | 819 | 820 | log "Running task $task ($taskScript)" 821 | 822 | 823 | 824 | if [ "sh" = "$realTaskExtension" ]; then 825 | . "$taskScript" 826 | else 827 | # running foreign script 828 | isHandled=1 829 | exportConfig 830 | case "$realTaskExtension" in 831 | php) 832 | __vars=$(php -f "$taskScript") 833 | ;; 834 | py) 835 | __vars=$(python "$taskScript") 836 | ;; 837 | rb) 838 | __vars=$(ruby "$taskScript") 839 | ;; 840 | pl) 841 | __vars=$(perl "$taskScript") 842 | ;; 843 | *) 844 | error "The $realTaskExtension extension is not handled. Email me if you think it should be." 845 | isHandled=0 846 | ;; 847 | esac 848 | 849 | if [ 1 -eq $isHandled ]; then 850 | processScriptOutput "$__vars" 851 | fi 852 | fi 853 | fi 854 | 855 | 856 | else 857 | error "Script not found: $taskScript" 858 | fi 859 | else 860 | log "skipping task ${task} by the underscore convention" 861 | fi 862 | done 863 | 864 | done 865 | else 866 | error "Config file not found: $configFile" 867 | fi 868 | done 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | exit $RETURN_CODE 879 | 880 | -------------------------------------------------------------------------------- /code/bash_manager_core-1.01.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ############################################################ 12 | # BASH MANAGER 1.01 - 2015-09-21 13 | # By LingTalfi 14 | ############################################################ 15 | major=${BASH_VERSION:0:1} 16 | if [ $major -lt 4 ]; then 17 | echo "This programs requires a minimum verson of bash 4 (your version is $BASH_VERSION)" 18 | exit 1 19 | fi 20 | 21 | ############################################################ 22 | # ERROR POLICY IN A NUTSHELL 23 | ############################################################ 24 | # 25 | # In short, my advice for task creators: should you use error or warning? 26 | # -> Use error if something that the maintainer wouldn't expect occurs 27 | # -> User warning for yourself while debugging your own task 28 | # 29 | # 30 | # The script (and probably any script by the way) has three possible issues: 31 | # 32 | # - success 33 | # - failure 34 | # - sucess with recoverable failures 35 | # 36 | # In case of success, there is not much to say. 37 | # In case of failure, the error message is sent to STDERR. 38 | # In case of recoverable failures, the message is also sent to 39 | # STDERR (because the admin should be able to see it), but 40 | # we also ask this question: 41 | # should we continue the script anyway or exit? 42 | # The answer depends on the user, so we provide the 43 | # STRICT_MODE variable for that, which can take one of 44 | # two values: 45 | # 0 (default): the script will always try to continue 46 | # if possible. 47 | # 1: the script exits when the first error is encountered. 48 | # 49 | # Failure and recoverable failures are handled by the error function. 50 | # 51 | # At our disposal, we have the following functions: 52 | # - log (outputs to STDOUT, only if VERBOSE=1) 53 | # - warning (outputs to STDOUT in orange) 54 | # - error (failure, recoverable failure) 55 | # 56 | # The log function displays a message only if VERBOSE=1. 57 | # By default, VERBOSE=0. 58 | # The user can set VERBOSE=1 by using the -v option on the command line. 59 | # 60 | # The warning is like the log, but the text is orange. 61 | # However, the warning always shows up, without respect to the VERBOSE option. 62 | # 63 | # The error method sends a message to STDERR. 64 | # Also, if STRICT_MODE=1, the script will stop immediately. 65 | # By default, STRICT_MODE=0. 66 | # The user can set STRICT_MODE=0 by using the -s option on the command line. 67 | # 68 | # 69 | ############################################################ 70 | 71 | ############################################################ 72 | # internal variables, please do not modify 73 | ############################################################ 74 | RETURN_CODE=0 75 | ERROR_CODE=1 76 | VERBOSE=0 # make the log function output something 77 | STRICT_MODE=0 # if 1, the script exits when an error is triggered 78 | CONFIG_FILES=() 79 | TASKS_LIST=() 80 | TASKS_LIST_OPTIONS=() 81 | TASKS_NAMES=() # per config file 82 | PROJECTS_LIST=() 83 | PROJECTS_LIST_OPTIONS=() 84 | PROJECTS_NAMES=() # per config file 85 | declare -A _CONFIG # this contains the values from config.defaults, should never be touched 86 | declare -A CONFIG # every time a project is called, the CONFIG array is recreated: it's a copy of _CONFIG. 87 | # tasks can then update the CONFIG array in the scope of one project. 88 | # For instance, it is used by the depositories.sh task in order to allow the user to 89 | # choose a depository per project from the config file. 90 | declare -A VALUES # concerned task's values 91 | declare -A ALL_VALUES # other tasks' values, namespaced with the format taskName_key (instead of key) 92 | declare -A OTHER_VALUES # other project's values 93 | declare -A CONFIG_OPTIONS # config options set via the command line options 94 | 95 | 96 | COLOR_TASK='\033[1m' 97 | COLOR_IMPORTANT='\033[0;44;33m' 98 | COLOR_WARNING='\033[0;31m' 99 | COLOR_STOP='\033[0m' 100 | 101 | COL_IMPORTANT=$(echo -e "${COLOR_IMPORTANT}") 102 | COL_WARNING=$(echo -e "${COLOR_WARNING}") 103 | COL_STOP=$(echo -e "${COLOR_STOP}") 104 | 105 | 106 | 107 | #---------------------------------------- 108 | # config.defaults (and command line) specials 109 | # They all begin with an underscore 110 | #---------------------------------------- 111 | _program_name="bash manager" 112 | 113 | 114 | 115 | 116 | ############################################################ 117 | # Functions 118 | ############################################################ 119 | 120 | 121 | _newFileInitCpt (){ 122 | _newFileCpt=20 123 | } 124 | 125 | _newFileName (){ 126 | 127 | dir=${1:-/tmp} 128 | tmp=${dir}/$RANDOM/$RANDOM/$RANDOM${RANDOM}.txt 129 | # tmp=${dir}/x.txt 130 | if [ -e "$tmp" ]; then 131 | ((_newFileCpt--)) 132 | if [ $_newFileCpt -gt 0 ]; then 133 | _newFileName 134 | else 135 | _newFileInitCpt 136 | echo "newFileName: couldn't find an unique name after $_newFileCpt tries: aborting!" 2>&1 137 | return 1 138 | fi 139 | else 140 | mkdir -p ${tmp%/*} && touch "$tmp" 141 | if [ 0 -eq $? ]; then 142 | echo "$tmp" 143 | else 144 | echo "newFileName: error: couldn't create the file $tmp" 2>&1 145 | return 1 146 | fi 147 | fi 148 | } 149 | 150 | 151 | # Creates a new file name 152 | # 153 | # Usage: 154 | # 155 | # name=$(newFileName /tmp/files) 156 | # 157 | # if [ 0 -eq $? ]; then 158 | # echo "name=$name" 159 | # else 160 | # echo "error" 161 | # fi 162 | # 163 | # 164 | newFileName (){ 165 | _newFileInitCpt 166 | _newFileName "$1" 167 | } 168 | 169 | 170 | 171 | 172 | #---------------------------------------- 173 | # Use this function to split a string with delimiters. 174 | #---------------------------------------- 175 | # Splits the string with the given delimiter. 176 | # The results are put in the SPLITLINE_ARR array. 177 | # The delimiter is a string of length 1. 178 | # 179 | # There are two different cases: 180 | # 181 | # - if the delimiter is the first char of string: 182 | # then the keys of the SPLITLINE_ARR will 183 | # be those passed to the function (argNameN, ...) 184 | # and the function returns 1. 185 | # 186 | # - if the delimiter is not the first char of string, 187 | # the SPLITLINE_ARR is empty and the function 188 | # returns 0. 189 | # 190 | 191 | declare -A SPLITLINE_ARR 192 | splitLine () # ( string, delimiter [, argNameN ]* ) 193 | { 194 | unset SPLITLINE_ARR 195 | declare -gA SPLITLINE_ARR=[] 196 | string="$1" 197 | delimiter="$2" 198 | if [ "$delimiter" = "${string:0:1}" ]; then 199 | string="${string:1}" 200 | shift 201 | shift 202 | key="$1" 203 | local i 204 | i=1 205 | while [ -n "$key" ]; do 206 | key="$1" 207 | if [ -n "$key" ]; then 208 | value=$(echo "$string" | cut -d"${delimiter}" -f${i} | xargs) 209 | SPLITLINE_ARR["$key"]="$value" 210 | (( i++ )) 211 | shift 212 | fi 213 | done 214 | return 1; 215 | 216 | else 217 | SPLITLINE_ARR["_default"]="$string" 218 | return 0; 219 | fi 220 | 221 | } 222 | 223 | 224 | 225 | 226 | 227 | # Utils 228 | error () # ( message ) 229 | { 230 | echo -e "$_program_name: error: $1" >&2 231 | RETURN_CODE=$ERROR_CODE 232 | 233 | if [ 1 -eq $VERBOSE ]; then 234 | printTrace 235 | fi 236 | 237 | if [ 1 -eq $STRICT_MODE ]; then 238 | exit 1 239 | fi 240 | } 241 | 242 | 243 | 244 | warning () # ( message) 245 | { 246 | old=$VERBOSE 247 | VERBOSE=1 248 | log "$1" "1" 249 | VERBOSE=$old 250 | } 251 | 252 | confError () # () 253 | { 254 | error "Bad config: $1" 255 | } 256 | 257 | abort (){ 258 | error "Aborting..." 259 | exit "$ERROR_CODE" 260 | } 261 | 262 | 263 | # level=0 means no special color 264 | # level=1 means warning color 265 | # level=2 means important color 266 | log () # (message, level=0) 267 | { 268 | if [ 1 -eq $VERBOSE ]; then 269 | if [ -z "$2" ]; then 270 | echo -e "$1" | sed "s/^/$_program_name\(v\): /g" 271 | else 272 | if [ "1" = "$2" ]; then 273 | echo -e "$1" | sed -e "s/^.*$/${_program_name}\(v\): ${COL_WARNING}&${COL_STOP}/g" 274 | elif [ "2" = "$2" ]; then 275 | echo -e "$1" | sed -e "s/^.*$/${_program_name}\(v\): ${COL_IMPORTANT}&${COL_STOP}/g" 276 | fi 277 | fi 278 | fi 279 | } 280 | 281 | 282 | 283 | 284 | # outputs a list of elements of separated by a sep 285 | toList ()# ( arrayEls, ?sep ) 286 | { 287 | sep="${2:-, }" 288 | arr=("${!1}") 289 | i=0 290 | for path in "${arr[@]}"; do 291 | if [ $i -eq 1 ]; then 292 | echo -n "$sep" 293 | fi 294 | echo -n "$path" 295 | i=1 296 | done 297 | echo 298 | } 299 | 300 | 301 | # same as toList, but prints a header first 302 | printList ()# ( header, arrayEls, ?sep=", " ) 303 | { 304 | echo -n "$1" 305 | sep=${3:-, } 306 | toList "$2" "$sep" 307 | } 308 | 309 | printAssocArray ()# ( assocArrayName ) 310 | { 311 | var=$(declare -p "$1") 312 | eval "declare -A _arr="${var#*=} 313 | for k in "${!_arr[@]}"; do 314 | echo "$k: ${_arr[$k]}" 315 | done 316 | 317 | } 318 | 319 | # outputs an array as a stack beginning by a leading expression 320 | toStack ()# ( arrayEls, ?leader="-- ") 321 | { 322 | lead="${2:--- }" 323 | arr=("${!1}") 324 | echo 325 | for path in "${arr[@]}"; do 326 | echo "$lead$path" 327 | done 328 | } 329 | 330 | 331 | # same as toStack, but prints a header first 332 | printStack ()# ( header, arrayEls, ?leader="-- " ) 333 | { 334 | echo -n "$1" 335 | lead=${3:--- } 336 | toStack "$2" "$lead" 337 | } 338 | 339 | printStackOrList () 340 | { 341 | name=("${!2}") 342 | len="${#name[@]}" 343 | if [ $len -gt 2 ]; then 344 | printStack "$1" "$2" 345 | else 346 | third=${3:-, } 347 | printList "$1" "$2" 348 | fi 349 | } 350 | 351 | # This method should print ---non blank and comments stripped--- lines of the given config file 352 | printConfigLines () 353 | { 354 | while read line || [ -n "$line" ]; do 355 | 356 | # strip comments 357 | line=$(echo "$line" | cut -d# -f1 ) 358 | if [ -n "$line" ]; then 359 | echo "$line" 360 | fi 361 | done < "$1" 362 | } 363 | 364 | # Store the key and values found in configFile into the array which arrayEls is given 365 | collectConfig ()#( arrayName, configFile ) 366 | { 367 | arr=("$1") 368 | while read line 369 | do 370 | key=$(echo "$line" | cut -d= -f1 ) 371 | value=$(echo "$line" | cut -d= -f2- ) 372 | if [ ${#value} -gt 0 ]; then 373 | arr["$key"]="$value" 374 | fi 375 | done < <(printConfigLines "$2") 376 | } 377 | 378 | 379 | 380 | dumpAssoc ()# ( arrayName ) 381 | { 382 | title="${1^^}" 383 | echo 384 | echo "======= $title ========" 385 | printAssocArray "$1" 386 | echo "======================" 387 | echo 388 | } 389 | 390 | 391 | parseAllValues ()# ( configFile ) 392 | { 393 | configFile="$1" 394 | namespace="" 395 | lineno=1 396 | while read line || [ -n "$line" ]; do 397 | if [ -n "$line" ]; then 398 | line="$(echo $line | xargs)" # trimming 399 | 400 | # strip comments: lines which first char is a sharp (#) 401 | if ! [ '#' = "${line:0:1}" ]; then 402 | if [[ "$line" == *"="* ]]; then 403 | if [ -n "$namespace" ]; then 404 | key=$(echo "$line" | cut -d= -f1 ) 405 | value=$(echo "$line" | cut -d= -f2- ) 406 | ALL_VALUES["${namespace}_${key}"]="$value" 407 | 408 | inArray "$key" "${PROJECTS_NAMES[@]}" 409 | if [ 1 -eq $? ]; then 410 | PROJECTS_NAMES+=("$key") 411 | log "Project found: $key" 412 | fi 413 | 414 | else 415 | warning "No namespace found for the first lines of file $configFile" 416 | fi 417 | else 418 | # if the last char is colon (:) and the line doesn't contain an equal symbol(=) 419 | # then it defines a new namespace 420 | if [ ":" = "${line:${#line}-1:${#line}}" ]; then 421 | namespace="${line:0:${#line}-1}" 422 | log "Namespace found: $namespace" 423 | TASKS_NAMES+=("$namespace") 424 | else 425 | error "Unknown line type in file $configFile, line $lineno: ignoring" 426 | fi 427 | fi 428 | fi 429 | fi 430 | (( lineno++ )) 431 | done < "$configFile" 432 | } 433 | 434 | 435 | 436 | # We can use this function to do one of the following: 437 | # -- check if an associative array has a certain key inArray "myKey" "${!myArray[@]}" 438 | # -- check if an associative array contains a certain value inArray "myValue" "${myArray[@]}" 439 | inArray () # ( value, arrayKeysOrValues ) 440 | { 441 | local e 442 | for e in "${@:2}"; do 443 | [[ "$e" == "$1" ]] && return 0; 444 | done 445 | return 1 446 | } 447 | 448 | 449 | printTrace() # ( commandName?, exit=0? ) 450 | { 451 | 452 | m="" 453 | m+="Trace:\n" 454 | m+="----------------------\n" 455 | local frame=0 456 | last=0 457 | while [ 0 -eq $last ]; do 458 | line="$( caller $frame )" 459 | last=$? 460 | ((frame++)) 461 | if [ 0 -eq $last ]; then 462 | 463 | 464 | zline=$(echo "$line" | cut -d " " -f 1) 465 | function=$(echo "$line" | cut -d " " -f 2) 466 | file=$(echo "$line" | cut -d " " -f 3-) 467 | 468 | m+="function $function in file $file, line $zline\n" 469 | 470 | fi 471 | done 472 | echo -e "$m" 473 | } 474 | 475 | 476 | startTask () #( taskName ) 477 | { 478 | log "${COLOR_TASK}---- TASK: $1 ------------${COLOR_STOP}" 479 | } 480 | 481 | endTask ()#( taskName ) 482 | { 483 | log "${COLOR_TASK}---- ENDTASK: $1 ------------${COLOR_STOP}" 484 | } 485 | 486 | 487 | 488 | printDate () 489 | { 490 | echo $(date +"%Y-%m-%d__%H-%M") 491 | } 492 | 493 | # used by chronos scripts 494 | printCurrentTime () 495 | { 496 | echo $(date +"%Y-%m-%d %H:%M:%S") 497 | } 498 | 499 | 500 | 501 | # This function will export the CONFIG array for other scripting 502 | # languages, like php or python for instance. 503 | # variables are exported using the following format: 504 | # BASH_MANAGER_CONFIG_$KEY 505 | 506 | exportConfig ()# () 507 | { 508 | local KEY 509 | for key in "${!CONFIG[@]}"; do 510 | KEY=$(echo "$key" | tr '[:lower:]' '[:upper:]') 511 | export "BASH_MANAGER_CONFIG_${KEY}"="${CONFIG[$key]}" 512 | done 513 | } 514 | 515 | # This function work in pair with exportConfig. 516 | # What it does is process the output of a script coded in 517 | # another scripting language (php, python, perl...). 518 | # Such a script is called "foreign" script 519 | # There is a convention for those foreign scripts to be aware of: 520 | # - Every line should end with the carriage return 521 | # - a line starting with 522 | # log: 523 | # will be send to the bash manager log 524 | # 525 | # - foreign scripts can update the content of the CONFIG array. 526 | # a line with the following format: 527 | # BASH_MANAGER_CONFIG_$KEY=$VALUE 528 | 529 | # will add the key $KEY with value $VALUE to the 530 | # CONFIG array. 531 | # 532 | 533 | 534 | processScriptOutput () # ( vars ) 535 | { 536 | local isConf 537 | while read line 538 | do 539 | if [ "log:" = "${line:0:4}" ]; then 540 | log "${line:4}" 541 | elif [ "warning:" = "${line:0:8}" ]; then 542 | warning "${line:8}" 543 | elif [ "error:" = "${line:0:6}" ]; then 544 | error "${line:6}" 545 | elif [ "exit:" = "${line:0:5}" ]; then 546 | exit $(echo "$line" | cut -d: -f2) 547 | else 548 | isConf=0 549 | if [ "BASH_MANAGER_CONFIG_" = "${line:0:20}" ]; then 550 | if [[ "$line" == *"="* ]]; then 551 | value=$(echo "$line" | cut -d= -f2- ) 552 | key=$(echo "$line" | cut -d= -f1 ) 553 | key="${key:20}" 554 | CONFIG["$key"]="$value" 555 | isConf=1 556 | fi 557 | fi 558 | if [ 0 -eq $isConf ]; then 559 | echo "$line" 560 | fi 561 | fi 562 | done <<< "$1" 563 | } 564 | 565 | 566 | printRealTaskName () # ( taskString ) 567 | { 568 | echo "$1" | cut -d'(' -f1 569 | } 570 | 571 | printRealTaskExtension () # ( taskString ) 572 | { 573 | extension=sh 574 | ext=$(echo "$1" | cut -d'(' -s -f2) 575 | if [ -n "$ext" ]; then 576 | if [ ')' = "${ext:${#ext}-1:${#ext}}" ]; then 577 | echo "${ext:0:${#ext}-1}" 578 | return 0 579 | fi 580 | fi 581 | echo "$extension" 582 | } 583 | 584 | ############################################################ 585 | # MAIN SCRIPT 586 | ############################################################ 587 | 588 | 589 | #---------------------------------------- 590 | # Processing command line options 591 | #---------------------------------------- 592 | while :; do 593 | key="$1" 594 | case "$key" in 595 | --option-*=*) 596 | optionName=$(echo "$1" | cut -d= -f1) 597 | optionValue=$(echo "$1" | cut -d= -f2-) 598 | optionName="${optionName:9}" 599 | CONFIG_OPTIONS["$optionName"]="$optionValue" 600 | ;; 601 | -c) 602 | CONFIG_FILES+=("$2") 603 | shift 604 | ;; 605 | -h) 606 | _home=("$2") 607 | shift 608 | ;; 609 | -p) 610 | PROJECTS_LIST_OPTIONS+=("$2") 611 | shift 612 | ;; 613 | -s) 614 | STRICT_MODE=1 615 | ;; 616 | -t) 617 | TASKS_LIST_OPTIONS+=("$2") 618 | shift 619 | ;; 620 | -v) 621 | VERBOSE=1 622 | ;; 623 | *) 624 | break 625 | ;; 626 | esac 627 | shift 628 | done 629 | 630 | 631 | #---------------------------------------- 632 | # bash manager requires that the _home variable 633 | # is defined, either from the caller script, or with the command line options -h 634 | #---------------------------------------- 635 | if [ -z "$_home" ]; then 636 | error "variable _home not defined, you can set it via the -h option, or create a wrapper script which defines _home and sources this bash manager script" 637 | exit $ERROR_CODE 638 | fi 639 | cd "$_home" 640 | # resolve relative paths 641 | _home=$("pwd") 642 | 643 | 644 | #---------------------------------------- 645 | # Check the basic structure 646 | # - home path 647 | # ----- config.defaults 648 | # ----- config.d 649 | # ----- tasks.d 650 | #---------------------------------------- 651 | # Turning _home as an absolute path 652 | log "HOME is set to: $_home" 653 | 654 | configDefaults="$_home/config.defaults" 655 | configDir="$_home/config.d" 656 | tasksDir="$_home/tasks.d" 657 | 658 | if ! [ -f "$configDefaults" ]; then 659 | confError "Cannot find config.defaults file, check your _home variable (not found: $configDefaults)" 660 | 661 | elif ! [ -d "$configDir" ]; then 662 | confError "Cannot find config.d directory, check your _home variable (not found: $configDir)" 663 | elif ! [ -d "$tasksDir" ]; then 664 | confError "Cannot find tasks.d directory, check your _home variable (not found: $tasksDir)" 665 | fi 666 | [ $RETURN_CODE -eq 1 ] && abort 667 | 668 | 669 | 670 | 671 | 672 | #---------------------------------------- 673 | # Prepare _CONFIG from config.defaults 674 | #---------------------------------------- 675 | while read line 676 | do 677 | key=$(echo "$line" | cut -d= -f1 ) 678 | value=$(echo "$line" | cut -d= -f2- ) 679 | if [ ${#value} -gt 0 ]; then 680 | _CONFIG["$key"]="$value" 681 | fi 682 | done < <(printConfigLines "$configDefaults") 683 | 684 | 685 | for key in "${!CONFIG_OPTIONS[@]}"; do 686 | _CONFIG["$key"]="${CONFIG_OPTIONS[$key]}" 687 | done 688 | 689 | # we will add a special _HOME value for the tasks 690 | _CONFIG[_HOME]="$_home" 691 | 692 | 693 | 694 | 695 | 696 | #---------------------------------------- 697 | # Collecting configuration files. 698 | # If the user doesn't specify config files on the command line, 699 | # we use all the config files located in the HOME/config.d directory 700 | #---------------------------------------- 701 | cd "$configDir" 702 | if [ -z $CONFIG_FILES ]; then 703 | CONFIG_FILES=($(find . | grep '\.txt$')) 704 | else 705 | for i in "${!CONFIG_FILES[@]}"; do 706 | CONFIG_FILES[$i]="./${CONFIG_FILES[$i]}.txt" 707 | done 708 | fi 709 | 710 | 711 | 712 | #---------------------------------------- 713 | # Outputting some info on STDOUT 714 | #---------------------------------------- 715 | log "$(printStack 'Collecting config files: ' CONFIG_FILES[@])" 716 | if [ -z $TASKS_LIST_OPTIONS ]; then 717 | log "Collecting tasks: (all)" 718 | else 719 | log "$(printStack 'Collecting tasks: ' TASKS_LIST_OPTIONS[@])" 720 | fi 721 | if [ -z $PROJECTS_LIST_OPTIONS ]; then 722 | log "Collecting projects: (all)" 723 | else 724 | log "$(printStack 'Collecting projects: ' PROJECTS_LIST_OPTIONS[@])" 725 | fi 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | #---------------------------------------- 735 | # Spread special internal variables 736 | #---------------------------------------- 737 | if [ -n "${_CONFIG[_program_name]}" ]; then 738 | _program_name="${_CONFIG[_program_name]}" 739 | fi 740 | 741 | 742 | 743 | 744 | #---------------------------------------- 745 | # Processing all config files found, one after another. 746 | #---------------------------------------- 747 | for configFile in "${CONFIG_FILES[@]}"; do 748 | 749 | # we need to cd in configDir on every iteration, since tasks can cd too 750 | cd "$configDir" 751 | if [ -f "$configFile" ]; then 752 | log "Scanning config file $configFile" 753 | 754 | unset VALUES 755 | unset ALL_VALUES 756 | 757 | declare -A VALUES 758 | declare -A ALL_VALUES 759 | 760 | 761 | unset TASKS_NAMES 762 | TASKS_NAMES=() 763 | unset PROJECTS_NAMES 764 | PROJECTS_NAMES=() 765 | unset PROJECTS_LIST 766 | PROJECTS_LIST=() 767 | unset TASKS_LIST 768 | TASKS_LIST=() 769 | 770 | 771 | # For every config file, 772 | # we collect the following arrays: 773 | # - PROJECT_NAMES: all the projects found in the current config file (no duplicate), in order of appearance 774 | # - TASKS_NAME: all the tasks found in the current config file (no duplicate), in order of appearance 775 | # - ALL_VALUES, an array that contains all the values we found, and looks like this: 776 | # ALL_VALUES[task_project]=value 777 | # ALL_VALUES[task_project2]=value 778 | # ALL_VALUES[task2_project]=value 779 | # ... 780 | # ... 781 | # 782 | parseAllValues "$configFile" 783 | 784 | 785 | # Preparing TASKS_LIST 786 | # If no task is defined, 787 | # we use all tasks found in TASKS_NAME 788 | if [ 0 -eq ${#TASKS_LIST_OPTIONS[@]} ]; then 789 | for task in "${TASKS_NAMES[@]}"; do 790 | TASKS_LIST+=("$task") 791 | done 792 | else 793 | for task in "${TASKS_LIST_OPTIONS[@]}"; do 794 | TASKS_LIST+=("$task") 795 | done 796 | fi 797 | # dumpAssoc "TASKS_NAMES" 798 | # dumpAssoc "TASKS_LIST" 799 | 800 | 801 | # Preparing PROJECTS_LIST_OPTIONS 802 | # If no task is defined, 803 | # we use all tasks found in TASKS_NAME 804 | if [ 0 -eq ${#PROJECTS_LIST_OPTIONS[@]} ]; then 805 | for project in "${PROJECTS_NAMES[@]}"; do 806 | PROJECTS_LIST+=("$project") 807 | done 808 | else 809 | for project in "${PROJECTS_LIST_OPTIONS[@]}"; do 810 | PROJECTS_LIST+=("$project") 811 | done 812 | fi 813 | # dumpAssoc "PROJECTS_LIST" 814 | 815 | 816 | 817 | # processing the projects 818 | for project in "${PROJECTS_LIST[@]}"; do 819 | 820 | log "Processing project $project" "2" 821 | 822 | 823 | unset CONFIG 824 | unset OTHER_VALUES 825 | declare -A CONFIG 826 | declare -A OTHER_VALUES 827 | 828 | 829 | # creating a CONFIG copy for the tasks to use 830 | for key in "${!_CONFIG[@]}"; do 831 | CONFIG["$key"]="${_CONFIG[$key]}" 832 | done 833 | dumpAssoc "CONFIG" 834 | exit 835 | 836 | 837 | # preparing other values for this project 838 | plen=${#project} 839 | (( plen++ )) # add the underscore length 840 | for key in "${!ALL_VALUES[@]}"; do 841 | if [ "_$project" = "${key:${#key}-$plen}" ]; then 842 | ptask="${key:0:${#key}-$plen}" 843 | OTHER_VALUES["$ptask"]="${ALL_VALUES[$key]}" 844 | fi 845 | done 846 | # dumpAssoc "OTHER_VALUES" 847 | 848 | 849 | 850 | for task in "${TASKS_LIST[@]}"; do 851 | 852 | # Tasks which name begins with underscore are skipped 853 | # This is handy for quick testing 854 | if ! [ "_" = "${task:0:1}" ]; then 855 | 856 | 857 | # handling foreign script direct call 858 | # notation is: 859 | # taskName(extension) 860 | # 861 | # 862 | realTaskExtension=$(printRealTaskExtension "$task") 863 | realTask=$(printRealTaskName "$task") 864 | 865 | 866 | 867 | 868 | 869 | taskScript="$tasksDir/$realTask.${realTaskExtension}" 870 | if [ -f "$taskScript" ]; then 871 | 872 | # Prepare the values to pass to the script 873 | 874 | key="${task}_${project}" 875 | 876 | inArray "$key" "${!ALL_VALUES[@]}" 877 | if [ 0 -eq $? ]; then 878 | VALUE="${ALL_VALUES[$key]}" 879 | 880 | 881 | CONFIG[_VALUE]="$VALUE" 882 | 883 | 884 | log "Running task $task ($taskScript)" 885 | 886 | 887 | 888 | if [ "sh" = "$realTaskExtension" ]; then 889 | . "$taskScript" 890 | else 891 | # running foreign script 892 | isHandled=1 893 | exportConfig 894 | case "$realTaskExtension" in 895 | php) 896 | __vars=$(php -f "$taskScript") 897 | ;; 898 | py) 899 | __vars=$(python "$taskScript") 900 | ;; 901 | rb) 902 | __vars=$(ruby "$taskScript") 903 | ;; 904 | pl) 905 | __vars=$(perl "$taskScript") 906 | ;; 907 | *) 908 | error "The $realTaskExtension extension is not handled. Email me if you think it should be." 909 | isHandled=0 910 | ;; 911 | esac 912 | 913 | if [ 1 -eq $isHandled ]; then 914 | processScriptOutput "$__vars" 915 | fi 916 | fi 917 | fi 918 | 919 | 920 | else 921 | error "Script not found: $taskScript" 922 | fi 923 | else 924 | log "skipping task ${task} by the underscore convention" 925 | fi 926 | done 927 | 928 | done 929 | else 930 | error "Config file not found: $configFile" 931 | fi 932 | done 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | exit $RETURN_CODE 943 | 944 | -------------------------------------------------------------------------------- /code/bash_manager_core-1.02.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ############################################################ 12 | # BASH MANAGER 1.02 - 2015-09-21: 20:28 13 | # By LingTalfi 14 | ############################################################ 15 | major=${BASH_VERSION:0:1} 16 | if [ $major -lt 4 ]; then 17 | echo "This programs requires a minimum verson of bash 4 (your version is $BASH_VERSION)" 18 | exit 1 19 | fi 20 | 21 | ############################################################ 22 | # ERROR POLICY IN A NUTSHELL 23 | ############################################################ 24 | # 25 | # In short, my advice for task creators: should you use error or warning? 26 | # -> Use error if something that the maintainer wouldn't expect occurs 27 | # -> User warning for yourself while debugging your own task 28 | # 29 | # 30 | # The script (and probably any script by the way) has three possible issues: 31 | # 32 | # - success 33 | # - failure 34 | # - sucess with recoverable failures 35 | # 36 | # In case of success, there is not much to say. 37 | # In case of failure, the error message is sent to STDERR. 38 | # In case of recoverable failures, the message is also sent to 39 | # STDERR (because the admin should be able to see it), but 40 | # we also ask this question: 41 | # should we continue the script anyway or exit? 42 | # The answer depends on the user, so we provide the 43 | # STRICT_MODE variable for that, which can take one of 44 | # two values: 45 | # 0 (default): the script will always try to continue 46 | # if possible. 47 | # 1: the script exits when the first error is encountered. 48 | # 49 | # Failure and recoverable failures are handled by the error function. 50 | # 51 | # At our disposal, we have the following functions: 52 | # - log (outputs to STDOUT, only if VERBOSE=1) 53 | # - warning (outputs to STDOUT in orange) 54 | # - error (failure, recoverable failure) 55 | # 56 | # The log function displays a message only if VERBOSE=1. 57 | # By default, VERBOSE=0. 58 | # The user can set VERBOSE=1 by using the -v option on the command line. 59 | # 60 | # The warning is like the log, but the text is orange. 61 | # However, the warning always shows up, without respect to the VERBOSE option. 62 | # 63 | # The error method sends a message to STDERR. 64 | # Also, if STRICT_MODE=1, the script will stop immediately. 65 | # By default, STRICT_MODE=0. 66 | # The user can set STRICT_MODE=0 by using the -s option on the command line. 67 | # 68 | # 69 | ############################################################ 70 | 71 | ############################################################ 72 | # internal variables, please do not modify 73 | ############################################################ 74 | RETURN_CODE=0 75 | ERROR_CODE=1 76 | VERBOSE=0 # make the log function output something 77 | STRICT_MODE=0 # if 1, the script exits when an error is triggered 78 | CONFIG_FILES=() 79 | TASKS_LIST=() 80 | TASKS_LIST_OPTIONS=() 81 | TASKS_NAMES=() # per config file 82 | PROJECTS_LIST=() 83 | PROJECTS_LIST_OPTIONS=() 84 | PROJECTS_NAMES=() # per config file 85 | declare -A _CONFIG # this contains the values from config.defaults, should never be touched 86 | declare -A CONFIG # every time a project is called, the CONFIG array is recreated: it's a copy of _CONFIG. 87 | # tasks can then update the CONFIG array in the scope of one project. 88 | # For instance, it is used by the depositories.sh task in order to allow the user to 89 | # choose a depository per project from the config file. 90 | declare -A VALUES # concerned task's values 91 | declare -A ALL_VALUES # other tasks' values, namespaced with the format taskName_key (instead of key) 92 | declare -A OTHER_VALUES # other project's values 93 | declare -A CONFIG_OPTIONS # config options set via the command line options 94 | 95 | 96 | COLOR_TASK='\033[1m' 97 | COLOR_IMPORTANT='\033[0;44;33m' 98 | COLOR_WARNING='\033[0;31m' 99 | COLOR_STOP='\033[0m' 100 | 101 | COL_IMPORTANT=$(echo -e "${COLOR_IMPORTANT}") 102 | COL_WARNING=$(echo -e "${COLOR_WARNING}") 103 | COL_STOP=$(echo -e "${COLOR_STOP}") 104 | 105 | 106 | 107 | #---------------------------------------- 108 | # config.defaults (and command line) specials 109 | # They all begin with an underscore 110 | #---------------------------------------- 111 | _program_name="bash manager" 112 | 113 | 114 | 115 | 116 | ############################################################ 117 | # Functions 118 | ############################################################ 119 | 120 | 121 | _newFileInitCpt (){ 122 | _newFileCpt=20 123 | } 124 | 125 | _newFileName (){ 126 | 127 | dir=${1:-/tmp} 128 | tmp=${dir}/$RANDOM/$RANDOM/$RANDOM${RANDOM}.txt 129 | # tmp=${dir}/x.txt 130 | if [ -e "$tmp" ]; then 131 | ((_newFileCpt--)) 132 | if [ $_newFileCpt -gt 0 ]; then 133 | _newFileName 134 | else 135 | _newFileInitCpt 136 | echo "newFileName: couldn't find an unique name after $_newFileCpt tries: aborting!" 2>&1 137 | return 1 138 | fi 139 | else 140 | mkdir -p ${tmp%/*} && touch "$tmp" 141 | if [ 0 -eq $? ]; then 142 | echo "$tmp" 143 | else 144 | echo "newFileName: error: couldn't create the file $tmp" 2>&1 145 | return 1 146 | fi 147 | fi 148 | } 149 | 150 | 151 | # Creates a new file name 152 | # 153 | # Usage: 154 | # 155 | # name=$(newFileName /tmp/files) 156 | # 157 | # if [ 0 -eq $? ]; then 158 | # echo "name=$name" 159 | # else 160 | # echo "error" 161 | # fi 162 | # 163 | # 164 | newFileName (){ 165 | _newFileInitCpt 166 | _newFileName "$1" 167 | } 168 | 169 | 170 | 171 | 172 | #---------------------------------------- 173 | # Use this function to split a string with delimiters. 174 | #---------------------------------------- 175 | # Splits the string with the given delimiter. 176 | # The results are put in the SPLITLINE_ARR array. 177 | # The delimiter is a string of length 1. 178 | # 179 | # There are two different cases: 180 | # 181 | # - if the delimiter is the first char of string: 182 | # then the keys of the SPLITLINE_ARR will 183 | # be those passed to the function (argNameN, ...) 184 | # and the function returns 1. 185 | # 186 | # - if the delimiter is not the first char of string, 187 | # the SPLITLINE_ARR is empty and the function 188 | # returns 0. 189 | # 190 | 191 | declare -A SPLITLINE_ARR 192 | splitLine () # ( string, delimiter [, argNameN ]* ) 193 | { 194 | unset SPLITLINE_ARR 195 | declare -gA SPLITLINE_ARR=[] 196 | string="$1" 197 | delimiter="$2" 198 | if [ "$delimiter" = "${string:0:1}" ]; then 199 | string="${string:1}" 200 | shift 201 | shift 202 | key="$1" 203 | local i 204 | i=1 205 | while [ -n "$key" ]; do 206 | key="$1" 207 | if [ -n "$key" ]; then 208 | value=$(echo "$string" | cut -d"${delimiter}" -f${i} | xargs) 209 | SPLITLINE_ARR["$key"]="$value" 210 | (( i++ )) 211 | shift 212 | fi 213 | done 214 | return 1; 215 | 216 | else 217 | SPLITLINE_ARR["_default"]="$string" 218 | return 0; 219 | fi 220 | 221 | } 222 | 223 | 224 | 225 | 226 | 227 | # Utils 228 | error () # ( message ) 229 | { 230 | echo -e "$_program_name: error: $1" >&2 231 | RETURN_CODE=$ERROR_CODE 232 | 233 | if [ 1 -eq $VERBOSE ]; then 234 | printTrace 235 | fi 236 | 237 | if [ 1 -eq $STRICT_MODE ]; then 238 | exit 1 239 | fi 240 | } 241 | 242 | 243 | 244 | warning () # ( message) 245 | { 246 | old=$VERBOSE 247 | VERBOSE=1 248 | log "$1" "1" 249 | VERBOSE=$old 250 | } 251 | 252 | confError () # () 253 | { 254 | error "Bad config: $1" 255 | } 256 | 257 | abort (){ 258 | error "Aborting..." 259 | exit "$ERROR_CODE" 260 | } 261 | 262 | 263 | # level=0 means no special color 264 | # level=1 means warning color 265 | # level=2 means important color 266 | log () # (message, level=0) 267 | { 268 | if [ 1 -eq $VERBOSE ]; then 269 | if [ -z "$2" ]; then 270 | echo -e "$1" | sed "s/^/$_program_name\(v\): /g" 271 | else 272 | if [ "1" = "$2" ]; then 273 | echo -e "$1" | sed -e "s/^.*$/${_program_name}\(v\): ${COL_WARNING}&${COL_STOP}/g" 274 | elif [ "2" = "$2" ]; then 275 | echo -e "$1" | sed -e "s/^.*$/${_program_name}\(v\): ${COL_IMPORTANT}&${COL_STOP}/g" 276 | fi 277 | fi 278 | fi 279 | } 280 | 281 | 282 | 283 | 284 | # outputs a list of elements of separated by a sep 285 | toList ()# ( arrayEls, ?sep ) 286 | { 287 | sep="${2:-, }" 288 | arr=("${!1}") 289 | i=0 290 | for path in "${arr[@]}"; do 291 | if [ $i -eq 1 ]; then 292 | echo -n "$sep" 293 | fi 294 | echo -n "$path" 295 | i=1 296 | done 297 | echo 298 | } 299 | 300 | 301 | # same as toList, but prints a header first 302 | printList ()# ( header, arrayEls, ?sep=", " ) 303 | { 304 | echo -n "$1" 305 | sep=${3:-, } 306 | toList "$2" "$sep" 307 | } 308 | 309 | printAssocArray ()# ( assocArrayName ) 310 | { 311 | var=$(declare -p "$1") 312 | eval "declare -A _arr="${var#*=} 313 | for k in "${!_arr[@]}"; do 314 | echo "$k: ${_arr[$k]}" 315 | done 316 | 317 | } 318 | 319 | # outputs an array as a stack beginning by a leading expression 320 | toStack ()# ( arrayEls, ?leader="-- ") 321 | { 322 | lead="${2:--- }" 323 | arr=("${!1}") 324 | echo 325 | for path in "${arr[@]}"; do 326 | echo "$lead$path" 327 | done 328 | } 329 | 330 | 331 | # same as toStack, but prints a header first 332 | printStack ()# ( header, arrayEls, ?leader="-- " ) 333 | { 334 | echo -n "$1" 335 | lead=${3:--- } 336 | toStack "$2" "$lead" 337 | } 338 | 339 | printStackOrList () 340 | { 341 | name=("${!2}") 342 | len="${#name[@]}" 343 | if [ $len -gt 2 ]; then 344 | printStack "$1" "$2" 345 | else 346 | third=${3:-, } 347 | printList "$1" "$2" 348 | fi 349 | } 350 | 351 | # This method should print ---non blank and comments stripped--- lines of the given config file 352 | printConfigLines () 353 | { 354 | while read line || [ -n "$line" ]; do 355 | 356 | # strip comments 357 | line=$(echo "$line" | cut -d# -f1 ) 358 | if [ -n "$line" ]; then 359 | echo "$line" 360 | fi 361 | done < "$1" 362 | } 363 | 364 | # Store the key and values found in configFile into the array which arrayEls is given 365 | collectConfig ()#( arrayName, configFile ) 366 | { 367 | arr=("$1") 368 | while read line 369 | do 370 | key=$(echo "$line" | cut -d= -f1 ) 371 | value=$(echo "$line" | cut -d= -f2- ) 372 | if [ ${#value} -gt 0 ]; then 373 | arr["$key"]="$value" 374 | fi 375 | done < <(printConfigLines "$2") 376 | } 377 | 378 | 379 | 380 | dumpAssoc ()# ( arrayName ) 381 | { 382 | title="${1^^}" 383 | echo 384 | echo "======= $title ========" 385 | printAssocArray "$1" 386 | echo "======================" 387 | echo 388 | } 389 | 390 | 391 | parseAllValues ()# ( configFile ) 392 | { 393 | configFile="$1" 394 | namespace="" 395 | lineno=1 396 | while read line || [ -n "$line" ]; do 397 | if [ -n "$line" ]; then 398 | line="$(echo $line | xargs)" # trimming 399 | 400 | # strip comments: lines which first char is a sharp (#) 401 | if ! [ '#' = "${line:0:1}" ]; then 402 | if [[ "$line" == *"="* ]]; then 403 | if [ -n "$namespace" ]; then 404 | key=$(echo "$line" | cut -d= -f1 ) 405 | value=$(echo "$line" | cut -d= -f2- ) 406 | ALL_VALUES["${namespace}_${key}"]="$value" 407 | 408 | inArray "$key" "${PROJECTS_NAMES[@]}" 409 | if [ 1 -eq $? ]; then 410 | PROJECTS_NAMES+=("$key") 411 | log "Project found: $key" 412 | fi 413 | 414 | else 415 | warning "No namespace found for the first lines of file $configFile" 416 | fi 417 | else 418 | # if the last char is colon (:) and the line doesn't contain an equal symbol(=) 419 | # then it defines a new namespace 420 | if [ ":" = "${line:${#line}-1:${#line}}" ]; then 421 | namespace="${line:0:${#line}-1}" 422 | log "Namespace found: $namespace" 423 | TASKS_NAMES+=("$namespace") 424 | else 425 | error "Unknown line type in file $configFile, line $lineno: ignoring" 426 | fi 427 | fi 428 | fi 429 | fi 430 | (( lineno++ )) 431 | done < "$configFile" 432 | } 433 | 434 | 435 | 436 | # We can use this function to do one of the following: 437 | # -- check if an associative array has a certain key inArray "myKey" "${!myArray[@]}" 438 | # -- check if an associative array contains a certain value inArray "myValue" "${myArray[@]}" 439 | inArray () # ( value, arrayKeysOrValues ) 440 | { 441 | local e 442 | for e in "${@:2}"; do 443 | [[ "$e" == "$1" ]] && return 0; 444 | done 445 | return 1 446 | } 447 | 448 | 449 | 450 | printTrace() # ( commandName?, exit=0? ) 451 | { 452 | 453 | m="" 454 | m+="Trace:\n" 455 | m+="----------------------\n" 456 | local frame=0 457 | last=0 458 | while [ 0 -eq $last ]; do 459 | line="$( caller $frame )" 460 | last=$? 461 | ((frame++)) 462 | if [ 0 -eq $last ]; then 463 | 464 | 465 | zline=$(echo "$line" | cut -d " " -f 1) 466 | function=$(echo "$line" | cut -d " " -f 2) 467 | file=$(echo "$line" | cut -d " " -f 3-) 468 | 469 | m+="function $function in file $file, line $zline\n" 470 | 471 | fi 472 | done 473 | echo -e "$m" 474 | } 475 | 476 | 477 | startTask () #( taskName ) 478 | { 479 | log "${COLOR_TASK}---- TASK: $1 ------------${COLOR_STOP}" 480 | } 481 | 482 | endTask ()#( taskName ) 483 | { 484 | len=${#1} 485 | (( n=7 + $len + 16)) 486 | echo "n=$n" 487 | # log "${COLOR_TASK}---- ENDTASK: $1 ------------${COLOR_STOP}" 488 | log "${COLOR_TASK}----------------${COLOR_STOP}" 489 | } 490 | 491 | 492 | 493 | printDate () 494 | { 495 | echo $(date +"%Y-%m-%d__%H-%M") 496 | } 497 | 498 | # used by chronos scripts 499 | printCurrentTime () 500 | { 501 | echo $(date +"%Y-%m-%d %H:%M:%S") 502 | } 503 | 504 | 505 | 506 | # This function will export the CONFIG array for other scripting 507 | # languages, like php or python for instance. 508 | # variables are exported using the following format: 509 | # BASH_MANAGER_CONFIG_$KEY 510 | 511 | exportConfig ()# () 512 | { 513 | local KEY 514 | for key in "${!CONFIG[@]}"; do 515 | KEY=$(echo "$key" | tr '[:lower:]' '[:upper:]') 516 | export "BASH_MANAGER_CONFIG_${KEY}"="${CONFIG[$key]}" 517 | done 518 | } 519 | 520 | # This function work in pair with exportConfig. 521 | # What it does is process the output of a script coded in 522 | # another scripting language (php, python, perl...). 523 | # Such a script is called "foreign" script 524 | # There is a convention for those foreign scripts to be aware of: 525 | # - Every line should end with the carriage return 526 | # - a line starting with 527 | # log: 528 | # will be send to the bash manager log 529 | # 530 | # - foreign scripts can update the content of the CONFIG array. 531 | # a line with the following format: 532 | # BASH_MANAGER_CONFIG_$KEY=$VALUE 533 | 534 | # will add the key $KEY with value $VALUE to the 535 | # CONFIG array. 536 | # 537 | 538 | 539 | processScriptOutput () # ( vars ) 540 | { 541 | local isConf 542 | while read line 543 | do 544 | if [ "log:" = "${line:0:4}" ]; then 545 | log "${line:4}" 546 | elif [ "warning:" = "${line:0:8}" ]; then 547 | warning "${line:8}" 548 | elif [ "error:" = "${line:0:6}" ]; then 549 | error "${line:6}" 550 | elif [ "exit:" = "${line:0:5}" ]; then 551 | exit $(echo "$line" | cut -d: -f2) 552 | else 553 | isConf=0 554 | if [ "BASH_MANAGER_CONFIG_" = "${line:0:20}" ]; then 555 | if [[ "$line" == *"="* ]]; then 556 | value=$(echo "$line" | cut -d= -f2- ) 557 | key=$(echo "$line" | cut -d= -f1 ) 558 | key="${key:20}" 559 | CONFIG["$key"]="$value" 560 | isConf=1 561 | fi 562 | fi 563 | if [ 0 -eq $isConf ]; then 564 | echo "$line" 565 | fi 566 | fi 567 | done <<< "$1" 568 | } 569 | 570 | 571 | printRealTaskName () # ( taskString ) 572 | { 573 | echo "$1" | cut -d'(' -f1 574 | } 575 | 576 | printRealTaskExtension () # ( taskString ) 577 | { 578 | extension=sh 579 | ext=$(echo "$1" | cut -d'(' -s -f2) 580 | if [ -n "$ext" ]; then 581 | if [ ')' = "${ext:${#ext}-1:${#ext}}" ]; then 582 | echo "${ext:0:${#ext}-1}" 583 | return 0 584 | fi 585 | fi 586 | echo "$extension" 587 | } 588 | 589 | ############################################################ 590 | # MAIN SCRIPT 591 | ############################################################ 592 | 593 | 594 | #---------------------------------------- 595 | # Processing command line options 596 | #---------------------------------------- 597 | while :; do 598 | key="$1" 599 | case "$key" in 600 | --option-*=*) 601 | optionName=$(echo "$1" | cut -d= -f1) 602 | optionValue=$(echo "$1" | cut -d= -f2-) 603 | optionName="${optionName:9}" 604 | CONFIG_OPTIONS["$optionName"]="$optionValue" 605 | ;; 606 | -c) 607 | CONFIG_FILES+=("$2") 608 | shift 609 | ;; 610 | -h) 611 | _home=("$2") 612 | shift 613 | ;; 614 | -p) 615 | PROJECTS_LIST_OPTIONS+=("$2") 616 | shift 617 | ;; 618 | -s) 619 | STRICT_MODE=1 620 | ;; 621 | -t) 622 | TASKS_LIST_OPTIONS+=("$2") 623 | shift 624 | ;; 625 | -v) 626 | VERBOSE=1 627 | ;; 628 | *) 629 | break 630 | ;; 631 | esac 632 | shift 633 | done 634 | 635 | 636 | #---------------------------------------- 637 | # bash manager requires that the _home variable 638 | # is defined, either from the caller script, or with the command line options -h 639 | #---------------------------------------- 640 | if [ -z "$_home" ]; then 641 | error "variable _home not defined, you can set it via the -h option, or create a wrapper script which defines _home and sources this bash manager script" 642 | exit $ERROR_CODE 643 | fi 644 | cd "$_home" 645 | # resolve relative paths 646 | _home=$("pwd") 647 | 648 | 649 | #---------------------------------------- 650 | # Check the basic structure 651 | # - home path 652 | # ----- config.defaults 653 | # ----- config.d 654 | # ----- tasks.d 655 | #---------------------------------------- 656 | # Turning _home as an absolute path 657 | log "HOME is set to: $_home" 658 | 659 | configDefaults="$_home/config.defaults" 660 | configDir="$_home/config.d" 661 | tasksDir="$_home/tasks.d" 662 | 663 | if ! [ -f "$configDefaults" ]; then 664 | confError "Cannot find config.defaults file, check your _home variable (not found: $configDefaults)" 665 | 666 | elif ! [ -d "$configDir" ]; then 667 | confError "Cannot find config.d directory, check your _home variable (not found: $configDir)" 668 | elif ! [ -d "$tasksDir" ]; then 669 | confError "Cannot find tasks.d directory, check your _home variable (not found: $tasksDir)" 670 | fi 671 | [ $RETURN_CODE -eq 1 ] && abort 672 | 673 | 674 | 675 | 676 | 677 | #---------------------------------------- 678 | # Prepare _CONFIG from config.defaults 679 | #---------------------------------------- 680 | while read line 681 | do 682 | key=$(echo "$line" | cut -d= -f1 ) 683 | value=$(echo "$line" | cut -d= -f2- ) 684 | if [ ${#value} -gt 0 ]; then 685 | _CONFIG["$key"]="$value" 686 | fi 687 | done < <(printConfigLines "$configDefaults") 688 | 689 | 690 | for key in "${!CONFIG_OPTIONS[@]}"; do 691 | _CONFIG["$key"]="${CONFIG_OPTIONS[$key]}" 692 | done 693 | 694 | # we will add a special _HOME value for the tasks 695 | _CONFIG[_HOME]="$_home" 696 | 697 | 698 | 699 | 700 | 701 | #---------------------------------------- 702 | # Collecting configuration files. 703 | # If the user doesn't specify config files on the command line, 704 | # we use all the config files located in the HOME/config.d directory 705 | #---------------------------------------- 706 | cd "$configDir" 707 | if [ -z $CONFIG_FILES ]; then 708 | CONFIG_FILES=($(find . | grep '\.txt$')) 709 | else 710 | for i in "${!CONFIG_FILES[@]}"; do 711 | CONFIG_FILES[$i]="./${CONFIG_FILES[$i]}.txt" 712 | done 713 | fi 714 | 715 | 716 | 717 | #---------------------------------------- 718 | # Outputting some info on STDOUT 719 | #---------------------------------------- 720 | log "$(printStack 'Collecting config files: ' CONFIG_FILES[@])" 721 | if [ -z $TASKS_LIST_OPTIONS ]; then 722 | log "Collecting tasks: (all)" 723 | else 724 | log "$(printStack 'Collecting tasks: ' TASKS_LIST_OPTIONS[@])" 725 | fi 726 | if [ -z $PROJECTS_LIST_OPTIONS ]; then 727 | log "Collecting projects: (all)" 728 | else 729 | log "$(printStack 'Collecting projects: ' PROJECTS_LIST_OPTIONS[@])" 730 | fi 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | #---------------------------------------- 740 | # Spread special internal variables 741 | #---------------------------------------- 742 | if [ -n "${_CONFIG[_program_name]}" ]; then 743 | _program_name="${_CONFIG[_program_name]}" 744 | fi 745 | 746 | 747 | 748 | 749 | #---------------------------------------- 750 | # Processing all config files found, one after another. 751 | #---------------------------------------- 752 | for configFile in "${CONFIG_FILES[@]}"; do 753 | 754 | # we need to cd in configDir on every iteration, since tasks can cd too 755 | cd "$configDir" 756 | if [ -f "$configFile" ]; then 757 | log "Scanning config file $configFile" 758 | 759 | unset VALUES 760 | unset ALL_VALUES 761 | 762 | declare -A VALUES 763 | declare -A ALL_VALUES 764 | 765 | 766 | unset TASKS_NAMES 767 | TASKS_NAMES=() 768 | unset PROJECTS_NAMES 769 | PROJECTS_NAMES=() 770 | unset PROJECTS_LIST 771 | PROJECTS_LIST=() 772 | unset TASKS_LIST 773 | TASKS_LIST=() 774 | 775 | 776 | # For every config file, 777 | # we collect the following arrays: 778 | # - PROJECT_NAMES: all the projects found in the current config file (no duplicate), in order of appearance 779 | # - TASKS_NAME: all the tasks found in the current config file (no duplicate), in order of appearance 780 | # - ALL_VALUES, an array that contains all the values we found, and looks like this: 781 | # ALL_VALUES[task_project]=value 782 | # ALL_VALUES[task_project2]=value 783 | # ALL_VALUES[task2_project]=value 784 | # ... 785 | # ... 786 | # 787 | parseAllValues "$configFile" 788 | 789 | 790 | # Preparing TASKS_LIST 791 | # If no task is defined, 792 | # we use all tasks found in TASKS_NAME 793 | if [ 0 -eq ${#TASKS_LIST_OPTIONS[@]} ]; then 794 | for task in "${TASKS_NAMES[@]}"; do 795 | TASKS_LIST+=("$task") 796 | done 797 | else 798 | for task in "${TASKS_LIST_OPTIONS[@]}"; do 799 | TASKS_LIST+=("$task") 800 | done 801 | fi 802 | # dumpAssoc "TASKS_NAMES" 803 | # dumpAssoc "TASKS_LIST" 804 | 805 | 806 | # Preparing PROJECTS_LIST_OPTIONS 807 | # If no task is defined, 808 | # we use all tasks found in TASKS_NAME 809 | if [ 0 -eq ${#PROJECTS_LIST_OPTIONS[@]} ]; then 810 | for project in "${PROJECTS_NAMES[@]}"; do 811 | PROJECTS_LIST+=("$project") 812 | done 813 | else 814 | for project in "${PROJECTS_LIST_OPTIONS[@]}"; do 815 | PROJECTS_LIST+=("$project") 816 | done 817 | fi 818 | # dumpAssoc "PROJECTS_LIST" 819 | 820 | 821 | 822 | # processing the projects 823 | for project in "${PROJECTS_LIST[@]}"; do 824 | 825 | log "Processing project $project" "2" 826 | 827 | 828 | unset CONFIG 829 | unset OTHER_VALUES 830 | declare -A CONFIG 831 | declare -A OTHER_VALUES 832 | 833 | 834 | # creating a CONFIG copy for the tasks to use 835 | for key in "${!_CONFIG[@]}"; do 836 | CONFIG["$key"]="${_CONFIG[$key]}" 837 | done 838 | # dumpAssoc "CONFIG" 839 | 840 | 841 | 842 | # preparing other values for this project 843 | plen=${#project} 844 | (( plen++ )) # add the underscore length 845 | for key in "${!ALL_VALUES[@]}"; do 846 | if [ "_$project" = "${key:${#key}-$plen}" ]; then 847 | ptask="${key:0:${#key}-$plen}" 848 | OTHER_VALUES["$ptask"]="${ALL_VALUES[$key]}" 849 | fi 850 | done 851 | # dumpAssoc "OTHER_VALUES" 852 | 853 | 854 | 855 | for task in "${TASKS_LIST[@]}"; do 856 | 857 | # Tasks which name begins with underscore are skipped 858 | # This is handy for quick testing 859 | if ! [ "_" = "${task:0:1}" ]; then 860 | 861 | 862 | # handling foreign script direct call 863 | # notation is: 864 | # taskName(extension) 865 | # 866 | # 867 | realTaskExtension=$(printRealTaskExtension "$task") 868 | realTask=$(printRealTaskName "$task") 869 | 870 | 871 | 872 | 873 | 874 | taskScript="$tasksDir/$realTask.${realTaskExtension}" 875 | if [ -f "$taskScript" ]; then 876 | 877 | # Prepare the values to pass to the script 878 | 879 | key="${task}_${project}" 880 | 881 | inArray "$key" "${!ALL_VALUES[@]}" 882 | if [ 0 -eq $? ]; then 883 | VALUE="${ALL_VALUES[$key]}" 884 | 885 | 886 | CONFIG[_VALUE]="$VALUE" 887 | 888 | 889 | # 1.02: override task's _VALUE from command line options 890 | # 891 | # the format is: 892 | # 893 | # --option-key=value 894 | # With key: 895 | # 896 | # <_VALUE_> <:projectName>? 897 | # 898 | # 899 | for ck in "${!CONFIG[@]}"; do 900 | if [ "_VALUE_" = "${ck:0:7}" ]; then 901 | tmpTaskName="${ck:7}" 902 | tmpProjectName="" 903 | if [[ "$tmpTaskName" == *":"* ]]; then 904 | tmpProjectName="${tmpTaskName#*:}" 905 | tmpTaskName="${tmpTaskName%%:*}" 906 | fi 907 | if [ "$tmpTaskName" = "$task" ]; then 908 | if [ -z "$tmpProjectName" -o "$tmpProjectName" = "$project" ]; then 909 | CONFIG[_VALUE]="${CONFIG[$ck]}" 910 | fi 911 | fi 912 | 913 | fi 914 | done 915 | # dumpAssoc "CONFIG" 916 | 917 | 918 | 919 | 920 | 921 | log "Running task $task ($taskScript)" 922 | 923 | 924 | 925 | if [ "sh" = "$realTaskExtension" ]; then 926 | . "$taskScript" 927 | else 928 | # running foreign script 929 | isHandled=1 930 | exportConfig 931 | case "$realTaskExtension" in 932 | php) 933 | __vars=$(php -f "$taskScript") 934 | ;; 935 | py) 936 | __vars=$(python "$taskScript") 937 | ;; 938 | rb) 939 | __vars=$(ruby "$taskScript") 940 | ;; 941 | pl) 942 | __vars=$(perl "$taskScript") 943 | ;; 944 | *) 945 | error "The $realTaskExtension extension is not handled. Email me if you think it should be." 946 | isHandled=0 947 | ;; 948 | esac 949 | 950 | if [ 1 -eq $isHandled ]; then 951 | processScriptOutput "$__vars" 952 | fi 953 | fi 954 | fi 955 | 956 | 957 | else 958 | error "Script not found: $taskScript" 959 | fi 960 | else 961 | log "skipping task ${task} by the underscore convention" 962 | fi 963 | done 964 | 965 | done 966 | else 967 | error "Config file not found: $configFile" 968 | fi 969 | done 970 | 971 | 972 | 973 | 974 | 975 | 976 | 977 | 978 | 979 | exit $RETURN_CODE 980 | 981 | -------------------------------------------------------------------------------- /code/bash_manager_core-1.03.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ############################################################ 12 | # BASH MANAGER 1.03 - 2015-09-21: 21:39 13 | # By LingTalfi 14 | ############################################################ 15 | major=${BASH_VERSION:0:1} 16 | if [ $major -lt 4 ]; then 17 | echo "This programs requires a minimum verson of bash 4 (your version is $BASH_VERSION)" 18 | exit 1 19 | fi 20 | 21 | ############################################################ 22 | # ERROR POLICY IN A NUTSHELL 23 | ############################################################ 24 | # 25 | # In short, my advice for task creators: should you use error or warning? 26 | # -> Use error if something that the maintainer wouldn't expect occurs 27 | # -> User warning for yourself while debugging your own task 28 | # 29 | # 30 | # The script (and probably any script by the way) has three possible issues: 31 | # 32 | # - success 33 | # - failure 34 | # - sucess with recoverable failures 35 | # 36 | # In case of success, there is not much to say. 37 | # In case of failure, the error message is sent to STDERR. 38 | # In case of recoverable failures, the message is also sent to 39 | # STDERR (because the admin should be able to see it), but 40 | # we also ask this question: 41 | # should we continue the script anyway or exit? 42 | # The answer depends on the user, so we provide the 43 | # STRICT_MODE variable for that, which can take one of 44 | # two values: 45 | # 0 (default): the script will always try to continue 46 | # if possible. 47 | # 1: the script exits when the first error is encountered. 48 | # 49 | # Failure and recoverable failures are handled by the error function. 50 | # 51 | # At our disposal, we have the following functions: 52 | # - log (outputs to STDOUT, only if VERBOSE=1) 53 | # - warning (outputs to STDOUT in orange) 54 | # - error (failure, recoverable failure) 55 | # 56 | # The log function displays a message only if VERBOSE=1. 57 | # By default, VERBOSE=0. 58 | # The user can set VERBOSE=1 by using the -v option on the command line. 59 | # 60 | # The warning is like the log, but the text is orange. 61 | # However, the warning always shows up, without respect to the VERBOSE option. 62 | # 63 | # The error method sends a message to STDERR. 64 | # Also, if STRICT_MODE=1, the script will stop immediately. 65 | # By default, STRICT_MODE=0. 66 | # The user can set STRICT_MODE=0 by using the -s option on the command line. 67 | # 68 | # 69 | ############################################################ 70 | 71 | ############################################################ 72 | # internal variables, please do not modify 73 | ############################################################ 74 | RETURN_CODE=0 75 | ERROR_CODE=1 76 | VERBOSE=0 # make the log function output something 77 | STRICT_MODE=0 # if 1, the script exits when an error is triggered 78 | CONFIG_FILES=() 79 | TASKS_LIST=() 80 | TASKS_LIST_OPTIONS=() 81 | TASKS_NAMES=() # per config file 82 | PROJECTS_LIST=() 83 | PROJECTS_LIST_OPTIONS=() 84 | PROJECTS_NAMES=() # per config file 85 | declare -A _CONFIG # this contains the values from config.defaults, should never be touched 86 | declare -A CONFIG # every time a project is called, the CONFIG array is recreated: it's a copy of _CONFIG. 87 | # tasks can then update the CONFIG array in the scope of one project. 88 | # For instance, it is used by the depositories.sh task in order to allow the user to 89 | # choose a depository per project from the config file. 90 | declare -A VALUES # concerned task's values 91 | declare -A ALL_VALUES # other tasks' values, namespaced with the format taskName_key (instead of key) 92 | declare -A OTHER_VALUES # other project's values 93 | declare -A CONFIG_OPTIONS # config options set via the command line options 94 | 95 | 96 | COLOR_TASK='\033[1m' 97 | COLOR_IMPORTANT='\033[0;44;33m' 98 | COLOR_WARNING='\033[0;31m' 99 | COLOR_STOP='\033[0m' 100 | 101 | COL_IMPORTANT=$(echo -e "${COLOR_IMPORTANT}") 102 | COL_WARNING=$(echo -e "${COLOR_WARNING}") 103 | COL_STOP=$(echo -e "${COLOR_STOP}") 104 | 105 | 106 | 107 | #---------------------------------------- 108 | # config.defaults (and command line) specials 109 | # They all begin with an underscore 110 | #---------------------------------------- 111 | _program_name="bash manager" 112 | 113 | 114 | 115 | 116 | ############################################################ 117 | # Functions 118 | ############################################################ 119 | 120 | 121 | strRepeat () # (char, howMany) 122 | { 123 | printf '%*s' $2 ''|tr ' ' "$1" 124 | } 125 | 126 | _newFileInitCpt (){ 127 | _newFileCpt=20 128 | } 129 | 130 | _newFileName (){ 131 | 132 | dir=${1:-/tmp} 133 | tmp=${dir}/$RANDOM/$RANDOM/$RANDOM${RANDOM}.txt 134 | # tmp=${dir}/x.txt 135 | if [ -e "$tmp" ]; then 136 | ((_newFileCpt--)) 137 | if [ $_newFileCpt -gt 0 ]; then 138 | _newFileName 139 | else 140 | _newFileInitCpt 141 | echo "newFileName: couldn't find an unique name after $_newFileCpt tries: aborting!" 2>&1 142 | return 1 143 | fi 144 | else 145 | mkdir -p ${tmp%/*} && touch "$tmp" 146 | if [ 0 -eq $? ]; then 147 | echo "$tmp" 148 | else 149 | echo "newFileName: error: couldn't create the file $tmp" 2>&1 150 | return 1 151 | fi 152 | fi 153 | } 154 | 155 | 156 | # Creates a new file name 157 | # 158 | # Usage: 159 | # 160 | # name=$(newFileName /tmp/files) 161 | # 162 | # if [ 0 -eq $? ]; then 163 | # echo "name=$name" 164 | # else 165 | # echo "error" 166 | # fi 167 | # 168 | # 169 | newFileName (){ 170 | _newFileInitCpt 171 | _newFileName "$1" 172 | } 173 | 174 | 175 | 176 | 177 | #---------------------------------------- 178 | # Use this function to split a string with delimiters. 179 | #---------------------------------------- 180 | # Splits the string with the given delimiter. 181 | # The results are put in the SPLITLINE_ARR array. 182 | # The delimiter is a string of length 1. 183 | # 184 | # There are two different cases: 185 | # 186 | # - if the delimiter is the first char of string: 187 | # then the keys of the SPLITLINE_ARR will 188 | # be those passed to the function (argNameN, ...) 189 | # and the function returns 1. 190 | # 191 | # - if the delimiter is not the first char of string, 192 | # the SPLITLINE_ARR is empty and the function 193 | # returns 0. 194 | # 195 | 196 | declare -A SPLITLINE_ARR 197 | splitLine () # ( string, delimiter [, argNameN ]* ) 198 | { 199 | unset SPLITLINE_ARR 200 | declare -gA SPLITLINE_ARR=[] 201 | string="$1" 202 | delimiter="$2" 203 | if [ "$delimiter" = "${string:0:1}" ]; then 204 | string="${string:1}" 205 | shift 206 | shift 207 | key="$1" 208 | local i 209 | i=1 210 | while [ -n "$key" ]; do 211 | key="$1" 212 | if [ -n "$key" ]; then 213 | value=$(echo "$string" | cut -d"${delimiter}" -f${i} | xargs) 214 | SPLITLINE_ARR["$key"]="$value" 215 | (( i++ )) 216 | shift 217 | fi 218 | done 219 | return 1; 220 | 221 | else 222 | SPLITLINE_ARR["_default"]="$string" 223 | return 0; 224 | fi 225 | 226 | } 227 | 228 | 229 | 230 | 231 | 232 | # Utils 233 | error () # ( message ) 234 | { 235 | echo -e "$_program_name: error: $1" >&2 236 | RETURN_CODE=$ERROR_CODE 237 | 238 | if [ 1 -eq $VERBOSE ]; then 239 | printTrace 240 | fi 241 | 242 | if [ 1 -eq $STRICT_MODE ]; then 243 | exit 1 244 | fi 245 | } 246 | 247 | 248 | 249 | warning () # ( message) 250 | { 251 | old=$VERBOSE 252 | VERBOSE=1 253 | log "$1" "1" 254 | VERBOSE=$old 255 | } 256 | 257 | confError () # () 258 | { 259 | error "Bad config: $1" 260 | } 261 | 262 | abort (){ 263 | error "Aborting..." 264 | exit "$ERROR_CODE" 265 | } 266 | 267 | 268 | # level=0 means no special color 269 | # level=1 means warning color 270 | # level=2 means important color 271 | log () # (message, level=0) 272 | { 273 | if [ 1 -eq $VERBOSE ]; then 274 | if [ -z "$2" ]; then 275 | echo -e "$1" | sed "s/^/$_program_name\(v\): /g" 276 | else 277 | if [ "1" = "$2" ]; then 278 | echo -e "$1" | sed -e "s/^.*$/${_program_name}\(v\): ${COL_WARNING}&${COL_STOP}/g" 279 | elif [ "2" = "$2" ]; then 280 | echo -e "$1" | sed -e "s/^.*$/${_program_name}\(v\): ${COL_IMPORTANT}&${COL_STOP}/g" 281 | fi 282 | fi 283 | fi 284 | } 285 | 286 | 287 | 288 | 289 | # outputs a list of elements of separated by a sep 290 | toList ()# ( arrayEls, ?sep ) 291 | { 292 | sep="${2:-, }" 293 | arr=("${!1}") 294 | i=0 295 | for path in "${arr[@]}"; do 296 | if [ $i -eq 1 ]; then 297 | echo -n "$sep" 298 | fi 299 | echo -n "$path" 300 | i=1 301 | done 302 | echo 303 | } 304 | 305 | 306 | # same as toList, but prints a header first 307 | printList ()# ( header, arrayEls, ?sep=", " ) 308 | { 309 | echo -n "$1" 310 | sep=${3:-, } 311 | toList "$2" "$sep" 312 | } 313 | 314 | printAssocArray ()# ( assocArrayName ) 315 | { 316 | var=$(declare -p "$1") 317 | eval "declare -A _arr="${var#*=} 318 | for k in "${!_arr[@]}"; do 319 | echo "$k: ${_arr[$k]}" 320 | done 321 | 322 | } 323 | 324 | # outputs an array as a stack beginning by a leading expression 325 | toStack ()# ( arrayEls, ?leader="-- ") 326 | { 327 | lead="${2:--- }" 328 | arr=("${!1}") 329 | echo 330 | for path in "${arr[@]}"; do 331 | echo "$lead$path" 332 | done 333 | } 334 | 335 | 336 | # same as toStack, but prints a header first 337 | printStack ()# ( header, arrayEls, ?leader="-- " ) 338 | { 339 | echo -n "$1" 340 | lead=${3:--- } 341 | toStack "$2" "$lead" 342 | } 343 | 344 | printStackOrList () 345 | { 346 | name=("${!2}") 347 | len="${#name[@]}" 348 | if [ $len -gt 2 ]; then 349 | printStack "$1" "$2" 350 | else 351 | third=${3:-, } 352 | printList "$1" "$2" 353 | fi 354 | } 355 | 356 | # This method should print ---non blank and comments stripped--- lines of the given config file 357 | printConfigLines () 358 | { 359 | while read line || [ -n "$line" ]; do 360 | 361 | # strip comments 362 | line=$(echo "$line" | cut -d# -f1 ) 363 | if [ -n "$line" ]; then 364 | echo "$line" 365 | fi 366 | done < "$1" 367 | } 368 | 369 | # Store the key and values found in configFile into the array which arrayEls is given 370 | collectConfig ()#( arrayName, configFile ) 371 | { 372 | arr=("$1") 373 | while read line 374 | do 375 | key=$(echo "$line" | cut -d= -f1 ) 376 | value=$(echo "$line" | cut -d= -f2- ) 377 | if [ ${#value} -gt 0 ]; then 378 | arr["$key"]="$value" 379 | fi 380 | done < <(printConfigLines "$2") 381 | } 382 | 383 | 384 | 385 | dumpAssoc ()# ( arrayName ) 386 | { 387 | title="${1^^}" 388 | echo 389 | echo "======= $title ========" 390 | printAssocArray "$1" 391 | echo "======================" 392 | echo 393 | } 394 | 395 | 396 | parseAllValues ()# ( configFile ) 397 | { 398 | configFile="$1" 399 | namespace="" 400 | lineno=1 401 | while read line || [ -n "$line" ]; do 402 | if [ -n "$line" ]; then 403 | line="$(echo $line | xargs)" # trimming 404 | 405 | # strip comments: lines which first char is a sharp (#) 406 | if ! [ '#' = "${line:0:1}" ]; then 407 | if [[ "$line" == *"="* ]]; then 408 | if [ -n "$namespace" ]; then 409 | key=$(echo "$line" | cut -d= -f1 ) 410 | value=$(echo "$line" | cut -d= -f2- ) 411 | ALL_VALUES["${namespace}_${key}"]="$value" 412 | 413 | inArray "$key" "${PROJECTS_NAMES[@]}" 414 | if [ 1 -eq $? ]; then 415 | PROJECTS_NAMES+=("$key") 416 | log "Project found: $key" 417 | fi 418 | 419 | else 420 | warning "No namespace found for the first lines of file $configFile" 421 | fi 422 | else 423 | # if the last char is colon (:) and the line doesn't contain an equal symbol(=) 424 | # then it defines a new namespace 425 | if [ ":" = "${line:${#line}-1:${#line}}" ]; then 426 | namespace="${line:0:${#line}-1}" 427 | log "Namespace found: $namespace" 428 | TASKS_NAMES+=("$namespace") 429 | else 430 | error "Unknown line type in file $configFile, line $lineno: ignoring" 431 | fi 432 | fi 433 | fi 434 | fi 435 | (( lineno++ )) 436 | done < "$configFile" 437 | } 438 | 439 | 440 | 441 | # We can use this function to do one of the following: 442 | # -- check if an associative array has a certain key inArray "myKey" "${!myArray[@]}" 443 | # -- check if an associative array contains a certain value inArray "myValue" "${myArray[@]}" 444 | inArray () # ( value, arrayKeysOrValues ) 445 | { 446 | local e 447 | for e in "${@:2}"; do 448 | [[ "$e" == "$1" ]] && return 0; 449 | done 450 | return 1 451 | } 452 | 453 | 454 | 455 | printTrace() # ( commandName?, exit=0? ) 456 | { 457 | 458 | m="" 459 | m+="Trace:\n" 460 | m+="----------------------\n" 461 | local frame=0 462 | last=0 463 | while [ 0 -eq $last ]; do 464 | line="$( caller $frame )" 465 | last=$? 466 | ((frame++)) 467 | if [ 0 -eq $last ]; then 468 | 469 | 470 | zline=$(echo "$line" | cut -d " " -f 1) 471 | function=$(echo "$line" | cut -d " " -f 2) 472 | file=$(echo "$line" | cut -d " " -f 3-) 473 | 474 | m+="function $function in file $file, line $zline\n" 475 | 476 | fi 477 | done 478 | echo -e "$m" 479 | } 480 | 481 | 482 | startTask () #( taskName ) 483 | { 484 | log "${COLOR_TASK}---- TASK: $1 ------------${COLOR_STOP}" 485 | } 486 | 487 | endTask ()#( taskName ) 488 | { 489 | len=${#1} 490 | (( n=7 + $len + 17)) 491 | m=$(strRepeat - "$n") 492 | # log "${COLOR_TASK}---- ENDTASK: $1 ------------${COLOR_STOP}" 493 | log "${COLOR_TASK}${m}${COLOR_STOP}" 494 | } 495 | 496 | 497 | 498 | printDate () 499 | { 500 | echo $(date +"%Y-%m-%d__%H-%M") 501 | } 502 | 503 | # used by chronos scripts 504 | printCurrentTime () 505 | { 506 | echo $(date +"%Y-%m-%d %H:%M:%S") 507 | } 508 | 509 | 510 | 511 | # This function will export the CONFIG array for other scripting 512 | # languages, like php or python for instance. 513 | # variables are exported using the following format: 514 | # BASH_MANAGER_CONFIG_$KEY 515 | 516 | exportConfig ()# () 517 | { 518 | local KEY 519 | for key in "${!CONFIG[@]}"; do 520 | KEY=$(echo "$key" | tr '[:lower:]' '[:upper:]') 521 | export "BASH_MANAGER_CONFIG_${KEY}"="${CONFIG[$key]}" 522 | done 523 | } 524 | 525 | # This function work in pair with exportConfig. 526 | # What it does is process the output of a script coded in 527 | # another scripting language (php, python, perl...). 528 | # Such a script is called "foreign" script 529 | # There is a convention for those foreign scripts to be aware of: 530 | # - Every line should end with the carriage return 531 | # - a line starting with 532 | # log: 533 | # will be send to the bash manager log 534 | # 535 | # - foreign scripts can update the content of the CONFIG array. 536 | # a line with the following format: 537 | # BASH_MANAGER_CONFIG_$KEY=$VALUE 538 | 539 | # will add the key $KEY with value $VALUE to the 540 | # CONFIG array. 541 | # 542 | 543 | 544 | processScriptOutput () # ( vars ) 545 | { 546 | local isConf 547 | while read line 548 | do 549 | if [ "log:" = "${line:0:4}" ]; then 550 | log "${line:4}" 551 | elif [ "warning:" = "${line:0:8}" ]; then 552 | warning "${line:8}" 553 | elif [ "error:" = "${line:0:6}" ]; then 554 | error "${line:6}" 555 | elif [ "exit:" = "${line:0:5}" ]; then 556 | exit $(echo "$line" | cut -d: -f2) 557 | else 558 | isConf=0 559 | if [ "BASH_MANAGER_CONFIG_" = "${line:0:20}" ]; then 560 | if [[ "$line" == *"="* ]]; then 561 | value=$(echo "$line" | cut -d= -f2- ) 562 | key=$(echo "$line" | cut -d= -f1 ) 563 | key="${key:20}" 564 | CONFIG["$key"]="$value" 565 | isConf=1 566 | fi 567 | fi 568 | if [ 0 -eq $isConf ]; then 569 | echo "$line" 570 | fi 571 | fi 572 | done <<< "$1" 573 | } 574 | 575 | 576 | printRealTaskName () # ( taskString ) 577 | { 578 | echo "$1" | cut -d'(' -f1 579 | } 580 | 581 | printRealTaskExtension () # ( taskString ) 582 | { 583 | extension=sh 584 | ext=$(echo "$1" | cut -d'(' -s -f2) 585 | if [ -n "$ext" ]; then 586 | if [ ')' = "${ext:${#ext}-1:${#ext}}" ]; then 587 | echo "${ext:0:${#ext}-1}" 588 | return 0 589 | fi 590 | fi 591 | echo "$extension" 592 | } 593 | 594 | ############################################################ 595 | # MAIN SCRIPT 596 | ############################################################ 597 | 598 | 599 | #---------------------------------------- 600 | # Processing command line options 601 | #---------------------------------------- 602 | while :; do 603 | key="$1" 604 | case "$key" in 605 | --option-*=*) 606 | optionName=$(echo "$1" | cut -d= -f1) 607 | optionValue=$(echo "$1" | cut -d= -f2-) 608 | optionName="${optionName:9}" 609 | CONFIG_OPTIONS["$optionName"]="$optionValue" 610 | ;; 611 | -c) 612 | CONFIG_FILES+=("$2") 613 | shift 614 | ;; 615 | -h) 616 | _home=("$2") 617 | shift 618 | ;; 619 | -p) 620 | PROJECTS_LIST_OPTIONS+=("$2") 621 | shift 622 | ;; 623 | -s) 624 | STRICT_MODE=1 625 | ;; 626 | -t) 627 | TASKS_LIST_OPTIONS+=("$2") 628 | shift 629 | ;; 630 | -v) 631 | VERBOSE=1 632 | ;; 633 | *) 634 | break 635 | ;; 636 | esac 637 | shift 638 | done 639 | 640 | 641 | #---------------------------------------- 642 | # bash manager requires that the _home variable 643 | # is defined, either from the caller script, or with the command line options -h 644 | #---------------------------------------- 645 | if [ -z "$_home" ]; then 646 | error "variable _home not defined, you can set it via the -h option, or create a wrapper script which defines _home and sources this bash manager script" 647 | exit $ERROR_CODE 648 | fi 649 | cd "$_home" 650 | # resolve relative paths 651 | _home=$("pwd") 652 | 653 | 654 | #---------------------------------------- 655 | # Check the basic structure 656 | # - home path 657 | # ----- config.defaults 658 | # ----- config.d 659 | # ----- tasks.d 660 | #---------------------------------------- 661 | # Turning _home as an absolute path 662 | log "HOME is set to: $_home" 663 | 664 | configDefaults="$_home/config.defaults" 665 | configDir="$_home/config.d" 666 | tasksDir="$_home/tasks.d" 667 | 668 | if ! [ -f "$configDefaults" ]; then 669 | confError "Cannot find config.defaults file, check your _home variable (not found: $configDefaults)" 670 | 671 | elif ! [ -d "$configDir" ]; then 672 | confError "Cannot find config.d directory, check your _home variable (not found: $configDir)" 673 | elif ! [ -d "$tasksDir" ]; then 674 | confError "Cannot find tasks.d directory, check your _home variable (not found: $tasksDir)" 675 | fi 676 | [ $RETURN_CODE -eq 1 ] && abort 677 | 678 | 679 | 680 | 681 | 682 | #---------------------------------------- 683 | # Prepare _CONFIG from config.defaults 684 | #---------------------------------------- 685 | while read line 686 | do 687 | key=$(echo "$line" | cut -d= -f1 ) 688 | value=$(echo "$line" | cut -d= -f2- ) 689 | if [ ${#value} -gt 0 ]; then 690 | _CONFIG["$key"]="$value" 691 | fi 692 | done < <(printConfigLines "$configDefaults") 693 | 694 | 695 | for key in "${!CONFIG_OPTIONS[@]}"; do 696 | _CONFIG["$key"]="${CONFIG_OPTIONS[$key]}" 697 | done 698 | 699 | # we will add a special _HOME value for the tasks 700 | _CONFIG[_HOME]="$_home" 701 | 702 | 703 | 704 | 705 | 706 | #---------------------------------------- 707 | # Collecting configuration files. 708 | # If the user doesn't specify config files on the command line, 709 | # we use all the config files located in the HOME/config.d directory 710 | #---------------------------------------- 711 | cd "$configDir" 712 | if [ -z $CONFIG_FILES ]; then 713 | CONFIG_FILES=($(find . | grep '\.txt$')) 714 | else 715 | for i in "${!CONFIG_FILES[@]}"; do 716 | CONFIG_FILES[$i]="./${CONFIG_FILES[$i]}.txt" 717 | done 718 | fi 719 | 720 | 721 | 722 | #---------------------------------------- 723 | # Outputting some info on STDOUT 724 | #---------------------------------------- 725 | log "$(printStack 'Collecting config files: ' CONFIG_FILES[@])" 726 | if [ -z $TASKS_LIST_OPTIONS ]; then 727 | log "Collecting tasks: (all)" 728 | else 729 | log "$(printStack 'Collecting tasks: ' TASKS_LIST_OPTIONS[@])" 730 | fi 731 | if [ -z $PROJECTS_LIST_OPTIONS ]; then 732 | log "Collecting projects: (all)" 733 | else 734 | log "$(printStack 'Collecting projects: ' PROJECTS_LIST_OPTIONS[@])" 735 | fi 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | #---------------------------------------- 745 | # Spread special internal variables 746 | #---------------------------------------- 747 | if [ -n "${_CONFIG[_program_name]}" ]; then 748 | _program_name="${_CONFIG[_program_name]}" 749 | fi 750 | 751 | 752 | 753 | 754 | #---------------------------------------- 755 | # Processing all config files found, one after another. 756 | #---------------------------------------- 757 | for configFile in "${CONFIG_FILES[@]}"; do 758 | 759 | # we need to cd in configDir on every iteration, since tasks can cd too 760 | cd "$configDir" 761 | if [ -f "$configFile" ]; then 762 | log "Scanning config file $configFile" 763 | 764 | unset VALUES 765 | unset ALL_VALUES 766 | 767 | declare -A VALUES 768 | declare -A ALL_VALUES 769 | 770 | 771 | unset TASKS_NAMES 772 | TASKS_NAMES=() 773 | unset PROJECTS_NAMES 774 | PROJECTS_NAMES=() 775 | unset PROJECTS_LIST 776 | PROJECTS_LIST=() 777 | unset TASKS_LIST 778 | TASKS_LIST=() 779 | 780 | 781 | # For every config file, 782 | # we collect the following arrays: 783 | # - PROJECT_NAMES: all the projects found in the current config file (no duplicate), in order of appearance 784 | # - TASKS_NAME: all the tasks found in the current config file (no duplicate), in order of appearance 785 | # - ALL_VALUES, an array that contains all the values we found, and looks like this: 786 | # ALL_VALUES[task_project]=value 787 | # ALL_VALUES[task_project2]=value 788 | # ALL_VALUES[task2_project]=value 789 | # ... 790 | # ... 791 | # 792 | parseAllValues "$configFile" 793 | 794 | 795 | # Preparing TASKS_LIST 796 | # If no task is defined, 797 | # we use all tasks found in TASKS_NAME 798 | if [ 0 -eq ${#TASKS_LIST_OPTIONS[@]} ]; then 799 | for task in "${TASKS_NAMES[@]}"; do 800 | TASKS_LIST+=("$task") 801 | done 802 | else 803 | for task in "${TASKS_LIST_OPTIONS[@]}"; do 804 | TASKS_LIST+=("$task") 805 | done 806 | fi 807 | # dumpAssoc "TASKS_NAMES" 808 | # dumpAssoc "TASKS_LIST" 809 | 810 | 811 | # Preparing PROJECTS_LIST_OPTIONS 812 | # If no task is defined, 813 | # we use all tasks found in TASKS_NAME 814 | if [ 0 -eq ${#PROJECTS_LIST_OPTIONS[@]} ]; then 815 | for project in "${PROJECTS_NAMES[@]}"; do 816 | PROJECTS_LIST+=("$project") 817 | done 818 | else 819 | for project in "${PROJECTS_LIST_OPTIONS[@]}"; do 820 | PROJECTS_LIST+=("$project") 821 | done 822 | fi 823 | # dumpAssoc "PROJECTS_LIST" 824 | 825 | 826 | 827 | # processing the projects 828 | for project in "${PROJECTS_LIST[@]}"; do 829 | 830 | log "Processing project $project" "2" 831 | 832 | 833 | unset CONFIG 834 | unset OTHER_VALUES 835 | declare -A CONFIG 836 | declare -A OTHER_VALUES 837 | 838 | 839 | # creating a CONFIG copy for the tasks to use 840 | for key in "${!_CONFIG[@]}"; do 841 | CONFIG["$key"]="${_CONFIG[$key]}" 842 | done 843 | # dumpAssoc "CONFIG" 844 | 845 | 846 | 847 | # preparing other values for this project 848 | plen=${#project} 849 | (( plen++ )) # add the underscore length 850 | for key in "${!ALL_VALUES[@]}"; do 851 | if [ "_$project" = "${key:${#key}-$plen}" ]; then 852 | ptask="${key:0:${#key}-$plen}" 853 | OTHER_VALUES["$ptask"]="${ALL_VALUES[$key]}" 854 | fi 855 | done 856 | # dumpAssoc "OTHER_VALUES" 857 | 858 | 859 | 860 | for task in "${TASKS_LIST[@]}"; do 861 | 862 | # Tasks which name begins with underscore are skipped 863 | # This is handy for quick testing 864 | if ! [ "_" = "${task:0:1}" ]; then 865 | 866 | 867 | # handling foreign script direct call 868 | # notation is: 869 | # taskName(extension) 870 | # 871 | # 872 | realTaskExtension=$(printRealTaskExtension "$task") 873 | realTask=$(printRealTaskName "$task") 874 | 875 | 876 | 877 | 878 | 879 | taskScript="$tasksDir/$realTask.${realTaskExtension}" 880 | if [ -f "$taskScript" ]; then 881 | 882 | # Prepare the values to pass to the script 883 | 884 | key="${task}_${project}" 885 | 886 | inArray "$key" "${!ALL_VALUES[@]}" 887 | if [ 0 -eq $? ]; then 888 | VALUE="${ALL_VALUES[$key]}" 889 | 890 | 891 | 892 | 893 | # 1.02: override task's _VALUE from command line options 894 | # 895 | # the format is: 896 | # 897 | # --option-key=value 898 | # With key: 899 | # 900 | # <_VALUE_> <:projectName>? 901 | # 902 | # 903 | for ck in "${!CONFIG[@]}"; do 904 | if [ "_VALUE_" = "${ck:0:7}" ]; then 905 | tmpTaskName="${ck:7}" 906 | tmpProjectName="" 907 | if [[ "$tmpTaskName" == *":"* ]]; then 908 | tmpProjectName="${tmpTaskName#*:}" 909 | tmpTaskName="${tmpTaskName%%:*}" 910 | fi 911 | if [ "$tmpTaskName" = "$task" ]; then 912 | if [ -z "$tmpProjectName" -o "$tmpProjectName" = "$project" ]; then 913 | VALUE="${CONFIG[$ck]}" 914 | fi 915 | fi 916 | 917 | fi 918 | done 919 | # dumpAssoc "CONFIG" 920 | 921 | 922 | CONFIG[_VALUE]="$VALUE" 923 | 924 | 925 | 926 | log "Running task $task ($taskScript)" 927 | 928 | 929 | 930 | if [ "sh" = "$realTaskExtension" ]; then 931 | . "$taskScript" 932 | else 933 | # running foreign script 934 | isHandled=1 935 | exportConfig 936 | case "$realTaskExtension" in 937 | php) 938 | __vars=$(php -f "$taskScript") 939 | ;; 940 | py) 941 | __vars=$(python "$taskScript") 942 | ;; 943 | rb) 944 | __vars=$(ruby "$taskScript") 945 | ;; 946 | pl) 947 | __vars=$(perl "$taskScript") 948 | ;; 949 | *) 950 | error "The $realTaskExtension extension is not handled. Email me if you think it should be." 951 | isHandled=0 952 | ;; 953 | esac 954 | 955 | if [ 1 -eq $isHandled ]; then 956 | processScriptOutput "$__vars" 957 | fi 958 | fi 959 | fi 960 | 961 | 962 | else 963 | error "Script not found: $taskScript" 964 | fi 965 | else 966 | log "skipping task ${task} by the underscore convention" 967 | fi 968 | done 969 | 970 | done 971 | else 972 | error "Config file not found: $configFile" 973 | fi 974 | done 975 | 976 | 977 | 978 | 979 | 980 | 981 | 982 | 983 | 984 | exit $RETURN_CODE 985 | 986 | -------------------------------------------------------------------------------- /code/bash_manager_core-1.04.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | 5 | 6 | 7 | ############################################################ 8 | # BASH MANAGER 1.04 - 2015-10-13 9 | # By LingTalfi 10 | ############################################################ 11 | major=${BASH_VERSION:0:1} 12 | if [ $major -lt 4 ]; then 13 | echo "This programs requires a minimum verson of bash 4 (your version is $BASH_VERSION)" 14 | exit 1 15 | fi 16 | 17 | ############################################################ 18 | # ERROR POLICY IN A NUTSHELL 19 | ############################################################ 20 | # 21 | # In short, my advice for task creators: should you use error or warning? 22 | # -> Use error if something that the maintainer wouldn't expect occurs 23 | # -> User warning for yourself while debugging your own task 24 | # 25 | # 26 | # The script (and probably any script by the way) has three possible issues: 27 | # 28 | # - success 29 | # - failure 30 | # - sucess with recoverable failures 31 | # 32 | # In case of success, there is not much to say. 33 | # In case of failure, the error message is sent to STDERR. 34 | # In case of recoverable failures, the message is also sent to 35 | # STDERR (because the admin should be able to see it), but 36 | # we also ask this question: 37 | # should we continue the script anyway or exit? 38 | # The answer depends on the user, so we provide the 39 | # STRICT_MODE variable for that, which can take one of 40 | # two values: 41 | # 0 (default): the script will always try to continue 42 | # if possible. 43 | # 1: the script exits when the first error is encountered. 44 | # 45 | # Failure and recoverable failures are handled by the error function. 46 | # 47 | # At our disposal, we have the following functions: 48 | # - log (outputs to STDOUT, only if VERBOSE=1) 49 | # - warning (outputs to STDOUT in orange) 50 | # - error (failure, recoverable failure) 51 | # 52 | # The log function displays a message only if VERBOSE=1. 53 | # By default, VERBOSE=0. 54 | # The user can set VERBOSE=1 by using the -v option on the command line. 55 | # 56 | # The warning is like the log, but the text is orange. 57 | # However, the warning always shows up, without respect to the VERBOSE option. 58 | # 59 | # The error method sends a message to STDERR. 60 | # Also, if STRICT_MODE=1, the script will stop immediately. 61 | # By default, STRICT_MODE=0. 62 | # The user can set STRICT_MODE=0 by using the -s option on the command line. 63 | # 64 | # 65 | ############################################################ 66 | 67 | ############################################################ 68 | # internal variables, please do not modify 69 | ############################################################ 70 | RETURN_CODE=0 71 | ERROR_CODE=1 72 | VERBOSE=0 # make the log function output something 73 | STRICT_MODE=0 # if 1, the script exits when an error is triggered 74 | CONFIG_FILES=() 75 | TASKS_LIST=() 76 | TASKS_LIST_OPTIONS=() 77 | TASKS_NAMES=() # per config file 78 | TASKS_SKIPPED=() 79 | TASKS_ALWAYS_INCLUDED=() 80 | PROJECTS_LIST=() 81 | PROJECTS_LIST_OPTIONS=() 82 | PROJECTS_NAMES=() # per config file 83 | EXPANDED_ARGS=() 84 | declare -A _CONFIG # this contains the values from config.defaults, should never be touched 85 | declare -A CONFIG # every time a project is called, the CONFIG array is recreated: it's a copy of _CONFIG. 86 | # tasks can then update the CONFIG array in the scope of one project. 87 | # For instance, it is used by the depositories.sh task in order to allow the user to 88 | # choose a depository per project from the config file. 89 | declare -A VALUES # concerned task's values 90 | declare -A ALL_VALUES # other tasks' values, namespaced with the format taskName_key (instead of key) 91 | declare -A OTHER_VALUES # other project's values 92 | declare -A CONFIG_OPTIONS # config options set via the command line options 93 | declare -A TASKS_EXTENSIONS # taskName => extension 94 | declare -A ALIASES 95 | 96 | 97 | 98 | COLOR_TASK='\033[1m' 99 | COLOR_IMPORTANT='\033[0;44;33m' 100 | COLOR_WARNING='\033[0;31m' 101 | COLOR_STOP='\033[0m' 102 | 103 | COL_IMPORTANT=$(echo -e "${COLOR_IMPORTANT}") 104 | COL_WARNING=$(echo -e "${COLOR_WARNING}") 105 | COL_STOP=$(echo -e "${COLOR_STOP}") 106 | 107 | aliasFiles=~/.bash_manager:/etc/.bash_manager 108 | 109 | 110 | 111 | 112 | #---------------------------------------- 113 | # config.defaults (and command line) specials 114 | # They all begin with an underscore 115 | #---------------------------------------- 116 | _program_name="bash manager" 117 | 118 | 119 | 120 | #---------------------------------------- 121 | # FUNCTIONS 122 | #---------------------------------------- 123 | _newFileInitCpt (){ 124 | _newFileCpt=20 125 | } 126 | 127 | 128 | _newFileName (){ 129 | 130 | dir=${1:-/tmp} 131 | tmp=${dir}/$RANDOM/$RANDOM/$RANDOM${RANDOM}.txt 132 | # tmp=${dir}/x.txt 133 | if [ -e "$tmp" ]; then 134 | ((_newFileCpt--)) 135 | if [ $_newFileCpt -gt 0 ]; then 136 | _newFileName 137 | else 138 | _newFileInitCpt 139 | echo "newFileName: couldn't find an unique name after $_newFileCpt tries: aborting!" 2>&1 140 | return 1 141 | fi 142 | else 143 | mkdir -p ${tmp%/*} && touch "$tmp" 144 | if [ 0 -eq $? ]; then 145 | echo "$tmp" 146 | else 147 | echo "newFileName: error: couldn't create the file $tmp" 2>&1 148 | return 1 149 | fi 150 | fi 151 | } 152 | 153 | abort (){ 154 | error "Aborting..." 155 | exit "$ERROR_CODE" 156 | } 157 | 158 | 159 | # Store the key and values found in configFile into the array which arrayEls is given 160 | collectConfig ()#( arrayName, configFile ) 161 | { 162 | arr=("$1") 163 | while read line 164 | do 165 | key=$(echo "$line" | cut -d= -f1 ) 166 | value=$(echo "$line" | cut -d= -f2- ) 167 | if [ ${#value} -gt 0 ]; then 168 | arr["$key"]="$value" 169 | fi 170 | done < <(printConfigLines "$2") 171 | } 172 | 173 | 174 | confError () # () 175 | { 176 | error "Bad config: $1" 177 | } 178 | 179 | createExpandedCommandLine(){ 180 | while :; do 181 | key="$1" 182 | 183 | if [ -z "$key" ]; then 184 | break 185 | fi 186 | 187 | case "$key" in 188 | *) 189 | add=$1 190 | found=0 191 | for k in "${!ALIASES[@]}"; do 192 | if [ "$k" = "$1" ]; then 193 | values="${ALIASES[$k]}" 194 | IFS=$' ' 195 | arr=( $values ) 196 | unset IFS 197 | for i in "${arr[@]}"; do 198 | EXPANDED_ARGS+=("$i") 199 | done 200 | found=1 201 | break 202 | fi 203 | done 204 | 205 | if [ 0 -eq $found ]; then 206 | EXPANDED_ARGS+=("$add") 207 | fi 208 | ;; 209 | esac 210 | shift 211 | done 212 | } 213 | 214 | 215 | dumpAssoc ()# ( arrayName ) 216 | { 217 | title="${1^^}" 218 | echo 219 | echo "======= $title ========" 220 | printAssocArray "$1" 221 | echo "======================" 222 | echo 223 | } 224 | 225 | 226 | endTask ()#( taskName ) 227 | { 228 | len=${#1} 229 | (( n=7 + $len + 17)) 230 | m=$(strRepeat - "$n") 231 | # log "${COLOR_TASK}---- ENDTASK: $1 ------------${COLOR_STOP}" 232 | log "${COLOR_TASK}${m}${COLOR_STOP}" 233 | } 234 | 235 | 236 | error () # ( message ) 237 | { 238 | echo -e "$_program_name: error: $1" >&2 239 | RETURN_CODE=$ERROR_CODE 240 | 241 | if [ 1 -eq $VERBOSE ]; then 242 | printTrace 243 | fi 244 | 245 | if [ 1 -eq $STRICT_MODE ]; then 246 | exit 1 247 | fi 248 | } 249 | 250 | 251 | # This function will export the variables array for other scripting 252 | # languages, like php or python for instance. 253 | # 254 | # For XXX array, where XXX can be one of CONFIG, OTHER_VALUES 255 | # keys will be: 256 | # BASH_MANAGER_XXX_$KEY 257 | # 258 | # keys are uppercased 259 | 260 | exportVars ()# () 261 | { 262 | local KEY 263 | for key in "${!CONFIG[@]}"; do 264 | KEY=$(echo "$key" | tr '[:lower:]' '[:upper:]') 265 | export "BASH_MANAGER_CONFIG_${KEY}"="${CONFIG[$key]}" 266 | done 267 | 268 | for key in "${!OTHER_VALUES[@]}"; do 269 | KEY=$(echo "$key" | tr '[:lower:]' '[:upper:]') 270 | export "BASH_MANAGER_OTHER_VALUES_${KEY}"="${CONFIG[$key]}" 271 | done 272 | } 273 | 274 | 275 | 276 | 277 | # We can use this function to do one of the following: 278 | # -- check if an associative array has a certain key inArray "myKey" "${!myArray[@]}" 279 | # -- check if an associative array contains a certain value inArray "myValue" "${myArray[@]}" 280 | # Beware: Returns 0 if value is in the array, and 1 if it is not!! 281 | inArray () # ( value, arrayKeysOrValues ) 282 | { 283 | local e 284 | for e in "${@:2}"; do 285 | [[ "$e" == "$1" ]] && return 0; 286 | done 287 | return 1 288 | } 289 | 290 | 291 | 292 | # level=0 means no special color 293 | # level=1 means warning color 294 | # level=2 means important color 295 | log () # (message, level=0) 296 | { 297 | if [ 1 -eq $VERBOSE ]; then 298 | if [ -z "$2" ]; then 299 | echo -e "$1" | sed "s/^/$_program_name\(v\): /g" 300 | else 301 | if [ "1" = "$2" ]; then 302 | echo -e "$1" | sed -e "s/^.*$/${_program_name}\(v\): ${COL_WARNING}&${COL_STOP}/g" 303 | elif [ "2" = "$2" ]; then 304 | echo -e "$1" | sed -e "s/^.*$/${_program_name}\(v\): ${COL_IMPORTANT}&${COL_STOP}/g" 305 | fi 306 | fi 307 | fi 308 | } 309 | 310 | # Creates a new file name 311 | # 312 | # Usage: 313 | # 314 | # name=$(newFileName /tmp/files) 315 | # 316 | # if [ 0 -eq $? ]; then 317 | # echo "name=$name" 318 | # else 319 | # echo "error" 320 | # fi 321 | # 322 | # 323 | newFileName (){ 324 | _newFileInitCpt 325 | _newFileName "$1" 326 | } 327 | 328 | 329 | # Parses a file like this: 330 | # 331 | # alias[myId]: 332 | # 333 | # sh = ssh komin 334 | # doo = rm -r /tmp/* 335 | # 336 | # 337 | # alias[myId2]: 338 | # 339 | # soo = -f rzog -t kabin 340 | # 341 | # 342 | # and creates an associative array. 343 | # For instance parseSection "alias" "myId" 344 | # returns the following array: 345 | # - sh => ssh komin 346 | # - doo => rm -r /tmp/* 347 | # 348 | # 349 | # 350 | parseAliases() # (chanel, sectionName) 351 | { 352 | channel=$1 353 | sectionName=$2 354 | search="$channel[$sectionName]:" 355 | lineno=1 356 | isStarted=0 357 | 358 | 359 | oldIfs="$IFS" 360 | 361 | IFS=$':' 362 | array=( $aliasFiles ) 363 | unset IFS 364 | 365 | 366 | for aliasFile in "${array[@]}"; do 367 | if [ -f "$aliasFile" ]; then 368 | while read line || [ -n "$line" ]; do 369 | if [ -n "$line" ]; then 370 | line="$(echo $line | xargs)" # trimming 371 | 372 | # strip comments: lines which first char is a sharp (#) 373 | if ! [ '#' = "${line:0:1}" ]; then 374 | 375 | if [ 0 -eq $isStarted ]; then 376 | if [ "$search" = "$line" ]; then 377 | isStarted=1 378 | fi 379 | else 380 | if [[ "$line" == *"="* ]]; then 381 | key=$(echo "$line" | cut -d= -f1 ) 382 | value=$(echo "$line" | cut -d= -f2- ) 383 | ALIASES[$(echo "$key" | xargs)]=$(echo "$value" | xargs) 384 | else 385 | # assuming section change (or could be syntax error) 386 | break 387 | fi 388 | fi 389 | fi 390 | fi 391 | (( lineno++ )) 392 | done < "$aliasFile" 393 | break 394 | fi 395 | done 396 | } 397 | 398 | 399 | 400 | parseAllValues ()# ( configFile ) 401 | { 402 | configFile="$1" 403 | namespace="" 404 | lineno=1 405 | while read line || [ -n "$line" ]; do 406 | if [ -n "$line" ]; then 407 | line="$(echo $line | xargs)" # trimming 408 | 409 | # strip comments: lines which first char is a sharp (#) 410 | if ! [ '#' = "${line:0:1}" ]; then 411 | if [[ "$line" == *"="* ]]; then 412 | if [ -n "$namespace" ]; then 413 | key=$(echo "$line" | cut -d= -f1 ) 414 | value=$(echo "$line" | cut -d= -f2- ) 415 | ALL_VALUES["${namespace}_${key}"]="$value" 416 | 417 | inArray "$key" "${PROJECTS_NAMES[@]}" 418 | if [ 1 -eq $? ]; then 419 | # wildcard is reserved: it means all project, therefore it's not a project name 420 | if [ '*' != "$key" ]; then 421 | PROJECTS_NAMES+=("$key") 422 | log "Project found: $key" 423 | fi 424 | fi 425 | 426 | else 427 | warning "No namespace found for the first lines of file $configFile" 428 | fi 429 | else 430 | # if the last char is colon (:) and the line doesn't contain an equal symbol(=) 431 | # then it defines a new namespace 432 | if [ ":" = "${line:${#line}-1:${#line}}" ]; then 433 | namespace="${line:0:${#line}-1}" 434 | isSkipped=0 435 | isAlwaysIncluded=0 436 | ext=sh 437 | 438 | 439 | 440 | 441 | # tasks defined in config file can use the _taskName notation, 442 | # which skips the task. However, the leading underscore is not part of the taskName 443 | if [ '_' = "${namespace:0:1}" ]; then 444 | namespace="${namespace:1}" 445 | isSkipped=1 446 | fi 447 | 448 | # tasks defined in config file can use the taskName* notation, 449 | # which always include the task. However, the trailing star is not part of the taskName 450 | if [ '*' = "${namespace:${#namespace}-1:1}" ]; then 451 | namespace="${namespace:0:${#namespace}-1}" 452 | isAlwaysIncluded=1 453 | fi 454 | 455 | 456 | 457 | # tasks defined in config file can use the taskName(extension) notation, 458 | # we keep extensions in a separate TASKS_EXTENSIONS array, which only 459 | # lists non sh extensions 460 | epos=$(strPos "$namespace" ")") 461 | if [ "-1" != "$epos" ]; then 462 | pos=$(strPos "$namespace" "(") 463 | if [ "-1" != "$pos" ]; then 464 | if [ $epos -gt $pos ]; then 465 | ext="${namespace:$pos+1:$epos-$pos-1}" 466 | namespace="${namespace:0:$pos}" 467 | 468 | fi 469 | fi 470 | fi 471 | 472 | 473 | log "Namespace found: $namespace" 474 | 475 | TASKS_NAMES+=("$namespace") 476 | if [ 1 -eq $isSkipped ]; then 477 | TASKS_SKIPPED+=("$namespace") 478 | elif [ 1 -eq $isAlwaysIncluded ]; then 479 | TASKS_ALWAYS_INCLUDED+=("$namespace") 480 | fi 481 | 482 | if [ 'sh' != "$ext" ]; then 483 | TASKS_EXTENSIONS["$namespace"]="$ext" 484 | fi 485 | 486 | else 487 | error "Unknown line type in file $configFile, line $lineno: ignoring" 488 | fi 489 | fi 490 | fi 491 | fi 492 | (( lineno++ )) 493 | done < "$configFile" 494 | } 495 | 496 | 497 | printAssocArray ()# ( assocArrayName ) 498 | { 499 | var=$(declare -p "$1") 500 | eval "declare -A _arr="${var#*=} 501 | for k in "${!_arr[@]}"; do 502 | echo "$k: ${_arr[$k]}" 503 | done 504 | 505 | } 506 | 507 | # This method should print ---non blank and comments stripped--- lines of the given config file 508 | printConfigLines () 509 | { 510 | 511 | while read line || [ -n "$line" ]; do 512 | 513 | # strip comments 514 | line=$(echo "$line" | cut -d# -f1 ) 515 | if [ -n "$line" ]; then 516 | echo "$line" 517 | fi 518 | done < "$1" 519 | } 520 | 521 | # used by chronos scripts 522 | printCurrentTime () 523 | { 524 | echo $(date +"%Y-%m-%d %H:%M:%S") 525 | } 526 | 527 | 528 | printDate () 529 | { 530 | echo $(date +"%Y-%m-%d__%H-%M") 531 | } 532 | 533 | 534 | 535 | # same as toList, but prints a header first 536 | printList ()# ( header, arrayEls, ?sep=", " ) 537 | { 538 | echo -n "$1" 539 | sep=${3:-, } 540 | toList "$2" "$sep" 541 | } 542 | 543 | printRealTaskExtension () # ( taskString ) 544 | { 545 | extension=sh 546 | for name in "${!TASKS_EXTENSIONS[@]}"; do 547 | if [ "$1" = "$name" ]; then 548 | extension="${TASKS_EXTENSIONS[$name]}" 549 | break 550 | fi 551 | done 552 | echo "$extension" 553 | } 554 | 555 | 556 | # same as toStack, but prints a header first 557 | printStack ()# ( header, arrayEls, ?leader="-- " ) 558 | { 559 | echo -n "$1" 560 | lead=${3:--- } 561 | toStack "$2" "$lead" 562 | } 563 | 564 | printStackOrList () 565 | { 566 | name=("${!2}") 567 | len="${#name[@]}" 568 | if [ $len -gt 2 ]; then 569 | printStack "$1" "$2" 570 | else 571 | third=${3:-, } 572 | printList "$1" "$2" 573 | fi 574 | } 575 | 576 | 577 | printTrace() # ( commandName?, exit=0? ) 578 | { 579 | 580 | m="" 581 | m+="Trace:\n" 582 | m+="----------------------\n" 583 | local frame=0 584 | last=0 585 | while [ 0 -eq $last ]; do 586 | line="$( caller $frame )" 587 | last=$? 588 | ((frame++)) 589 | if [ 0 -eq $last ]; then 590 | 591 | 592 | zline=$(echo "$line" | cut -d " " -f 1) 593 | function=$(echo "$line" | cut -d " " -f 2) 594 | file=$(echo "$line" | cut -d " " -f 3-) 595 | 596 | m+="function $function in file $file, line $zline\n" 597 | 598 | fi 599 | done 600 | echo -e "$m" 601 | } 602 | 603 | 604 | 605 | processCommandLine(){ 606 | 607 | while :; do 608 | key="$1" 609 | case "$key" in 610 | --option-*=*) 611 | optionName=$(echo "$1" | cut -d= -f1) 612 | optionValue=$(echo "$1" | cut -d= -f2-) 613 | optionName="${optionName:9}" 614 | CONFIG_OPTIONS["$optionName"]="$optionValue" 615 | ;; 616 | -c) 617 | CONFIG_FILES+=("$2") 618 | shift 619 | ;; 620 | -h) 621 | # actually, home has been previously processed 622 | # _home=("$2") 623 | shift 624 | ;; 625 | -p) 626 | PROJECTS_LIST_OPTIONS+=("$2") 627 | shift 628 | ;; 629 | -s) 630 | STRICT_MODE=1 631 | ;; 632 | -t) 633 | TASKS_LIST_OPTIONS+=("$2") 634 | shift 635 | ;; 636 | -v) 637 | VERBOSE=1 638 | ;; 639 | *) 640 | break 641 | ;; 642 | esac 643 | shift 644 | done 645 | } 646 | 647 | 648 | processHomeFromCommandLine(){ 649 | 650 | while :; do 651 | key="$1" 652 | case "$key" in 653 | -h) 654 | _home=("$2") 655 | shift 656 | ;; 657 | *) 658 | break 659 | ;; 660 | esac 661 | shift 662 | done 663 | } 664 | 665 | 666 | # This function work in pair with exportVars. 667 | # What it does is process the output of a script coded in 668 | # another scripting language (php, python, perl...). 669 | # Such a script is called "foreign" script 670 | # There is a convention for those foreign scripts to be aware of: 671 | # - Every line should end with the carriage return 672 | # - a line starting with 673 | # log: 674 | # will be send to the bash manager log 675 | # 676 | # - foreign scripts can update the content of the CONFIG array. 677 | # a line with the following format: 678 | # BASH_MANAGER_CONFIG_$KEY=$VALUE 679 | 680 | # will add the key $KEY with value $VALUE to the 681 | # CONFIG array. 682 | # 683 | processScriptOutput () # ( vars ) 684 | { 685 | local isConf 686 | while read line 687 | do 688 | if [ "log:" = "${line:0:4}" ]; then 689 | log "${line:4}" 690 | elif [ "warning:" = "${line:0:8}" ]; then 691 | warning "${line:8}" 692 | elif [ "error:" = "${line:0:6}" ]; then 693 | error "${line:6}" 694 | elif [ "startTask:" = "${line:0:10}" ]; then 695 | startTask "${line:10}" 696 | elif [ "endTask:" = "${line:0:8}" ]; then 697 | endTask "${line:8}" 698 | elif [ "exit:" = "${line:0:5}" ]; then 699 | exit $(echo "$line" | cut -d: -f2) 700 | else 701 | isConf=0 702 | if [ "BASH_MANAGER_CONFIG_" = "${line:0:20}" ]; then 703 | if [[ "$line" == *"="* ]]; then 704 | value=$(echo "$line" | cut -d= -f2- ) 705 | key=$(echo "$line" | cut -d= -f1 ) 706 | key="${key:20}" 707 | CONFIG["$key"]="$value" 708 | isConf=1 709 | fi 710 | fi 711 | if [ 0 -eq $isConf ]; then 712 | echo "$line" 713 | fi 714 | fi 715 | done <<< "$1" 716 | } 717 | 718 | 719 | 720 | #---------------------------------------- 721 | # Use this function to split a string with delimiters. 722 | #---------------------------------------- 723 | # Splits the string with the given delimiter. 724 | # The results are put in the SPLITLINE_ARR array. 725 | # The delimiter is a string of length 1. 726 | # 727 | # There are two different cases: 728 | # 729 | # - if the delimiter is the first char of string: 730 | # then the keys of the SPLITLINE_ARR will 731 | # be those passed to the function (argNameN, ...) 732 | # and the function returns 1. 733 | # 734 | # - if the delimiter is not the first char of string, 735 | # the SPLITLINE_ARR is empty and the function 736 | # returns 0. 737 | # 738 | 739 | declare -A SPLITLINE_ARR 740 | splitLine () # ( string, delimiter [, argNameN ]* ) 741 | { 742 | unset SPLITLINE_ARR 743 | declare -gA SPLITLINE_ARR=[] 744 | string="$1" 745 | delimiter="$2" 746 | if [ "$delimiter" = "${string:0:1}" ]; then 747 | string="${string:1}" 748 | shift 749 | shift 750 | key="$1" 751 | local i 752 | i=1 753 | while [ -n "$key" ]; do 754 | key="$1" 755 | if [ -n "$key" ]; then 756 | value=$(echo "$string" | cut -d"${delimiter}" -f${i} | xargs) 757 | SPLITLINE_ARR["$key"]="$value" 758 | (( i++ )) 759 | shift 760 | fi 761 | done 762 | return 1; 763 | 764 | else 765 | SPLITLINE_ARR["_default"]="$string" 766 | return 0; 767 | fi 768 | 769 | } 770 | 771 | startTask () #( taskName ) 772 | { 773 | log "${COLOR_TASK}---- TASK: $1 ------------${COLOR_STOP}" 774 | } 775 | 776 | strPos() { # ( haystack, needle ) 777 | x="${1%%$2*}" 778 | [[ $x = $1 ]] && echo -1 || echo ${#x} 779 | } 780 | 781 | strRepeat () # (char, howMany) 782 | { 783 | printf '%*s' $2 ''|tr ' ' "$1" 784 | } 785 | 786 | 787 | # outputs a list of elements of separated by a sep 788 | toList ()# ( arrayEls, ?sep ) 789 | { 790 | sep="${2:-, }" 791 | arr=("${!1}") 792 | i=0 793 | for path in "${arr[@]}"; do 794 | if [ $i -eq 1 ]; then 795 | echo -n "$sep" 796 | fi 797 | echo -n "$path" 798 | i=1 799 | done 800 | echo 801 | } 802 | 803 | # outputs an array as a stack beginning by a leading expression 804 | toStack ()# ( arrayEls, ?leader="-- ") 805 | { 806 | lead="${2:--- }" 807 | arr=("${!1}") 808 | echo 809 | for path in "${arr[@]}"; do 810 | echo "$lead$path" 811 | done 812 | } 813 | 814 | warning () # ( message) 815 | { 816 | old=$VERBOSE 817 | VERBOSE=1 818 | log "$1" "1" 819 | VERBOSE=$old 820 | } 821 | 822 | 823 | 824 | 825 | 826 | 827 | ############################################################ 828 | # MAIN SCRIPT 829 | ############################################################ 830 | 831 | 832 | #---------------------------------------- 833 | # First, we want to get the home parameter 834 | #---------------------------------------- 835 | # It can be passed by inclusion of this script, 836 | # or using the command line. 837 | # Below, we parse the command line 838 | processHomeFromCommandLine "$@" 839 | 840 | #---------------------------------------- 841 | # bash manager requires that the _home variable 842 | # is defined, either from the caller script, or with the command line options -h 843 | #---------------------------------------- 844 | if [ -z "$_home" ]; then 845 | error "variable _home not defined, you can set it via the -h option, or create a wrapper script which defines _home and sources this bash manager script" 846 | exit $ERROR_CODE 847 | fi 848 | cd "$_home" 849 | # resolve relative paths 850 | _home=$("pwd") 851 | 852 | 853 | #---------------------------------------- 854 | # Check the basic structure 855 | # - home path 856 | # ----- config.defaults 857 | # ----- config.d 858 | # ----- tasks.d 859 | #---------------------------------------- 860 | # Turning _home as an absolute path 861 | log "HOME is set to: $_home" 862 | 863 | configDefaults="$_home/config.defaults" 864 | configDir="$_home/config.d" 865 | tasksDir="$_home/tasks.d" 866 | 867 | if ! [ -f "$configDefaults" ]; then 868 | confError "Cannot find config.defaults file, check your _home variable (not found: $configDefaults)" 869 | 870 | elif ! [ -d "$configDir" ]; then 871 | confError "Cannot find config.d directory, check your _home variable (not found: $configDir)" 872 | elif ! [ -d "$tasksDir" ]; then 873 | confError "Cannot find tasks.d directory, check your _home variable (not found: $tasksDir)" 874 | fi 875 | [ $RETURN_CODE -eq 1 ] && abort 876 | 877 | 878 | #---------------------------------------- 879 | # Prepare _CONFIG from config.defaults 880 | #---------------------------------------- 881 | while read line 882 | do 883 | key=$(echo "$line" | cut -d= -f1 ) 884 | value=$(echo "$line" | cut -d= -f2- ) 885 | if [ ${#value} -gt 0 ]; then 886 | _CONFIG["$key"]="$value" 887 | fi 888 | done < <(printConfigLines "$configDefaults") 889 | 890 | 891 | for key in "${!CONFIG_OPTIONS[@]}"; do 892 | _CONFIG["$key"]="${CONFIG_OPTIONS[$key]}" 893 | done 894 | 895 | 896 | # we will add a special _HOME value for the tasks 897 | _CONFIG[_HOME]="$_home" 898 | 899 | 900 | #---------------------------------------- 901 | # Collecting configuration files. 902 | # If the user doesn't specify config files on the command line, 903 | # we use all the config files located in the HOME/config.d directory 904 | #---------------------------------------- 905 | cd "$configDir" 906 | if [ -z $CONFIG_FILES ]; then 907 | CONFIG_FILES=($(find . | grep '\.txt$')) 908 | else 909 | for i in "${!CONFIG_FILES[@]}"; do 910 | CONFIG_FILES[$i]="./${CONFIG_FILES[$i]}.txt" 911 | done 912 | fi 913 | 914 | 915 | 916 | 917 | #---------------------------------------- 918 | # Outputting some info on STDOUT 919 | #---------------------------------------- 920 | log "$(printStack 'Collecting config files: ' CONFIG_FILES[@])" 921 | if [ -z $TASKS_LIST_OPTIONS ]; then 922 | log "Collecting tasks: (all)" 923 | else 924 | log "$(printStack 'Collecting tasks: ' TASKS_LIST_OPTIONS[@])" 925 | fi 926 | if [ -z $PROJECTS_LIST_OPTIONS ]; then 927 | log "Collecting projects: (all)" 928 | else 929 | log "$(printStack 'Collecting projects: ' PROJECTS_LIST_OPTIONS[@])" 930 | fi 931 | 932 | 933 | 934 | #---------------------------------------- 935 | # Spread special internal variables 936 | #---------------------------------------- 937 | if [ -n "${_CONFIG[_program_name]}" ]; then 938 | _program_name="${_CONFIG[_program_name]}" 939 | fi 940 | 941 | 942 | 943 | #---------------------------------------- 944 | # Processing aliases 945 | #---------------------------------------- 946 | parseAliases "alias" "$_program_name" 947 | 948 | 949 | #---------------------------------------- 950 | # Processing command line options, 951 | # expanding aliases 952 | #---------------------------------------- 953 | createExpandedCommandLine "$@" 954 | 955 | 956 | processCommandLine "${EXPANDED_ARGS[@]}" 957 | 958 | 959 | 960 | 961 | 962 | 963 | 964 | 965 | 966 | 967 | 968 | 969 | 970 | 971 | 972 | 973 | 974 | #---------------------------------------- 975 | # THE MAIN LOOP 976 | #---------------------------------------- 977 | # Processing all config files found, one after another. 978 | #---------------------------------------- 979 | for configFile in "${CONFIG_FILES[@]}"; do 980 | 981 | # we need to cd in configDir on every iteration, since tasks can cd too 982 | cd "$configDir" 983 | if [ -f "$configFile" ]; then 984 | log "Scanning config file $configFile" 985 | 986 | unset VALUES 987 | unset ALL_VALUES 988 | 989 | declare -A VALUES 990 | declare -A ALL_VALUES 991 | 992 | 993 | unset TASKS_NAMES 994 | TASKS_NAMES=() 995 | unset PROJECTS_NAMES 996 | PROJECTS_NAMES=() 997 | unset PROJECTS_LIST 998 | PROJECTS_LIST=() 999 | unset TASKS_LIST 1000 | TASKS_LIST=() 1001 | 1002 | 1003 | # For every config file, 1004 | # we collect the following arrays: 1005 | # - PROJECT_NAMES: all the projects found in the current config file (no duplicate), in order of appearance 1006 | # - TASKS_NAME: all the tasks found in the current config file (no duplicate), in order of appearance 1007 | # - ALL_VALUES, an array that contains all the values we found, and looks like this: 1008 | # ALL_VALUES[task_project]=value 1009 | # ALL_VALUES[task_project2]=value 1010 | # ALL_VALUES[task2_project]=value 1011 | # ... 1012 | # ... 1013 | # 1014 | parseAllValues "$configFile" 1015 | # dumpAssoc "TASKS_EXTENSIONS" 1016 | # dumpAssoc "ALL_VALUES" 1017 | # dumpAssoc "PROJECTS_NAMES" 1018 | # dumpAssoc "TASKS_SKIPPED" 1019 | # dumpAssoc "TASKS_ALWAYS_INCLUDED" 1020 | 1021 | 1022 | # Preparing TASKS_LIST 1023 | # If no task is defined, 1024 | # we use all tasks found in TASKS_NAME 1025 | if [ 0 -eq ${#TASKS_LIST_OPTIONS[@]} ]; then 1026 | for task in "${TASKS_NAMES[@]}"; do 1027 | TASKS_LIST+=("$task") 1028 | done 1029 | else 1030 | for task in "${TASKS_LIST_OPTIONS[@]}"; do 1031 | TASKS_LIST+=("$task") 1032 | done 1033 | fi 1034 | 1035 | # add the tasks always included 1036 | for task in "${TASKS_ALWAYS_INCLUDED[@]}"; do 1037 | TASKS_LIST+=("$task") 1038 | done 1039 | 1040 | 1041 | # dumpAssoc "TASKS_NAMES" 1042 | # dumpAssoc "TASKS_LIST" 1043 | 1044 | 1045 | 1046 | 1047 | 1048 | # Preparing PROJECTS_LIST_OPTIONS 1049 | # If no task is defined, 1050 | # we use all tasks found in TASKS_NAME 1051 | if [ 0 -eq ${#PROJECTS_LIST_OPTIONS[@]} ]; then 1052 | for project in "${PROJECTS_NAMES[@]}"; do 1053 | PROJECTS_LIST+=("$project") 1054 | done 1055 | else 1056 | for project in "${PROJECTS_LIST_OPTIONS[@]}"; do 1057 | PROJECTS_LIST+=("$project") 1058 | done 1059 | fi 1060 | # dumpAssoc "PROJECTS_LIST" 1061 | 1062 | 1063 | 1064 | # processing the projects 1065 | for project in "${PROJECTS_LIST[@]}"; do 1066 | 1067 | log "Processing project $project" "2" 1068 | 1069 | 1070 | unset CONFIG 1071 | unset OTHER_VALUES 1072 | declare -A CONFIG 1073 | declare -A OTHER_VALUES 1074 | 1075 | 1076 | # creating a CONFIG copy for the tasks to use 1077 | for key in "${!_CONFIG[@]}"; do 1078 | CONFIG["$key"]="${_CONFIG[$key]}" 1079 | done 1080 | # dumpAssoc "CONFIG" 1081 | 1082 | 1083 | 1084 | # preparing other values for this project 1085 | plen=${#project} 1086 | (( plen++ )) # add the underscore length 1087 | for key in "${!ALL_VALUES[@]}"; do 1088 | if [ "_$project" = "${key:${#key}-$plen}" ]; then 1089 | ptask="${key:0:${#key}-$plen}" 1090 | OTHER_VALUES["$ptask"]="${ALL_VALUES[$key]}" 1091 | elif [ '_*' = "${key:${#key}-2}" ]; then 1092 | ptask="${key:0:${#key}-2}" 1093 | OTHER_VALUES["$ptask"]="${ALL_VALUES[$key]}" 1094 | fi 1095 | done 1096 | # dumpAssoc "OTHER_VALUES" 1097 | 1098 | 1099 | 1100 | for task in "${TASKS_NAMES[@]}"; do 1101 | 1102 | 1103 | inArray "$task" "${TASKS_LIST[@]}" 1104 | if [ 0 -eq $? ]; then 1105 | 1106 | # Tasks which name begins with underscore are skipped 1107 | # This is handy for quick testing 1108 | inArray "$task" "${TASKS_SKIPPED[@]}" 1109 | isSkipped=$? 1110 | 1111 | 1112 | if [ 1 -eq $isSkipped ]; then 1113 | 1114 | # handling foreign script direct call 1115 | # notation is: 1116 | # taskName(extension) 1117 | # 1118 | # 1119 | realTaskExtension=$(printRealTaskExtension "$task") 1120 | realTask=$task 1121 | 1122 | 1123 | 1124 | taskScript="$tasksDir/$realTask.${realTaskExtension}" 1125 | if [ -f "$taskScript" ]; then 1126 | 1127 | # Prepare the values to pass to the script 1128 | 1129 | 1130 | 1131 | inArray "$task" "${!OTHER_VALUES[@]}" 1132 | if [ 0 -eq $? ]; then 1133 | VALUE="${OTHER_VALUES[$task]}" 1134 | 1135 | 1136 | 1137 | 1138 | # 1.02: override task's _VALUE from command line options 1139 | # 1140 | # the format is: 1141 | # 1142 | # --option-key=value 1143 | # With key: 1144 | # 1145 | # <_VALUE_> <:projectName>? 1146 | # 1147 | # 1148 | for ck in "${!CONFIG[@]}"; do 1149 | if [ "_VALUE_" = "${ck:0:7}" ]; then 1150 | tmpTaskName="${ck:7}" 1151 | tmpProjectName="" 1152 | if [[ "$tmpTaskName" == *":"* ]]; then 1153 | tmpProjectName="${tmpTaskName#*:}" 1154 | tmpTaskName="${tmpTaskName%%:*}" 1155 | fi 1156 | if [ "$tmpTaskName" = "$task" ]; then 1157 | if [ -z "$tmpProjectName" -o "$tmpProjectName" = "$project" ]; then 1158 | VALUE="${CONFIG[$ck]}" 1159 | fi 1160 | fi 1161 | 1162 | fi 1163 | done 1164 | # dumpAssoc "CONFIG" 1165 | 1166 | 1167 | CONFIG[_VALUE]="$VALUE" 1168 | 1169 | 1170 | 1171 | log "Running task $task ($taskScript)" 1172 | 1173 | 1174 | # script should use only the following vars: 1175 | # - VALUE 1176 | # - CONFIG 1177 | # - CONFIG_OPTIONS 1178 | # - OTHER_VALUES 1179 | 1180 | if [ "sh" = "$realTaskExtension" ]; then 1181 | . "$taskScript" 1182 | else 1183 | # running foreign script 1184 | isHandled=1 1185 | exportVars 1186 | case "$realTaskExtension" in 1187 | php) 1188 | __vars=$(php -f "$taskScript") 1189 | ;; 1190 | py) 1191 | __vars=$(python "$taskScript") 1192 | ;; 1193 | rb) 1194 | __vars=$(ruby "$taskScript") 1195 | ;; 1196 | pl) 1197 | __vars=$(perl "$taskScript") 1198 | ;; 1199 | *) 1200 | error "The $realTaskExtension extension is not handled. Email me if you think it should be." 1201 | isHandled=0 1202 | ;; 1203 | esac 1204 | 1205 | if [ 1 -eq $isHandled ]; then 1206 | processScriptOutput "$__vars" 1207 | fi 1208 | fi 1209 | fi 1210 | 1211 | 1212 | else 1213 | error "Script not found: $taskScript" 1214 | fi 1215 | else 1216 | log "skipping task ${task} by the underscore convention" 1217 | fi 1218 | fi 1219 | done 1220 | 1221 | done 1222 | else 1223 | error "Config file not found: $configFile" 1224 | fi 1225 | done 1226 | 1227 | 1228 | 1229 | 1230 | 1231 | 1232 | 1233 | 1234 | 1235 | exit $RETURN_CODE 1236 | 1237 | -------------------------------------------------------------------------------- /code/tutorials/hello-world/config.d/hello.conf.txt: -------------------------------------------------------------------------------- 1 | hello: 2 | martin=coucou -------------------------------------------------------------------------------- /code/tutorials/hello-world/config.defaults: -------------------------------------------------------------------------------- 1 | _program_name=bash manager 2 | -------------------------------------------------------------------------------- /code/tutorials/hello-world/tasks.d/hello.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | 5 | 6 | 7 | startTask "Hello" 8 | echo 9 | echo 10 | echo 11 | echo "Hi, we are now inside the hello task!" 12 | echo "Obviously, this task doesn't do much, but it gives a basic idea of how tasks are implemented." 13 | echo "Every task has access to at least three variables:" 14 | echo " - VALUE, the specific value for this task (in the config file)" 15 | echo " - CONFIG, an array of configuration values, created using the HOME/config.defaults file" 16 | echo " and the command line options (--option-myKey=myValue)" 17 | echo " - OTHER_VALUES, an array containing all the task's values for this project (for the current config file)." 18 | echo " So that task can see each other's values if necessary" 19 | 20 | echo 21 | echo "Here are what those variables look like in the hello task:" 22 | echo "VALUE:" 23 | echo "-- $VALUE" 24 | echo "CONFIG:" 25 | for k in "${!CONFIG[@]}"; do 26 | echo "-- $k=${CONFIG[$k]}" 27 | done 28 | echo "OTHER_VALUES:" 29 | for k in "${!OTHER_VALUES[@]}"; do 30 | echo "-- $k=${OTHER_VALUES[$k]}" 31 | done 32 | 33 | 34 | echo 35 | echo 36 | echo "Please feel free to edit the HOME/config.defaults file and the HOME/config.d/hello.sh file and" 37 | echo "observe the change in the output." 38 | echo "Then you should feel ok to create your own tasks ;)" 39 | 40 | 41 | 42 | echo 43 | echo 44 | echo 45 | endTask "Hello" 46 | 47 | -------------------------------------------------------------------------------- /code/tutorials/the-flexible-software/config.d/me.txt: -------------------------------------------------------------------------------- 1 | t1: 2 | p1=123 3 | p2=456 4 | p3=789 5 | 6 | 7 | t2: 8 | p1=apple 9 | p3=cherry 10 | 11 | 12 | t3: 13 | p2=karate 14 | p3= 15 | 16 | 17 | t4: 18 | p1=square 19 | p2=oval 20 | 21 | 22 | t5: 23 | p1=red 24 | p2=green 25 | p3=blue 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /code/tutorials/the-flexible-software/config.defaults: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingtalfi/bashmanager/272db7908773c4ae217270d1c67f6e6fc4ba6d11/code/tutorials/the-flexible-software/config.defaults -------------------------------------------------------------------------------- /code/tutorials/the-flexible-software/tasks.d/t1.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | 5 | echo "$task: processing value $VALUE (project $project)" 6 | 7 | 8 | -------------------------------------------------------------------------------- /code/tutorials/the-flexible-software/tasks.d/t2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | 5 | echo "$task: processing value $VALUE (project $project)" -------------------------------------------------------------------------------- /code/tutorials/the-flexible-software/tasks.d/t3.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | 5 | echo "$task: processing value $VALUE (project $project)" 6 | -------------------------------------------------------------------------------- /code/tutorials/the-flexible-software/tasks.d/t4.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | 5 | echo "$task: processing value $VALUE (project $project)" -------------------------------------------------------------------------------- /code/tutorials/the-flexible-software/tasks.d/t5.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | 5 | echo "$task: processing value $VALUE (project $project)" -------------------------------------------------------------------------------- /code/utils/new_app.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | # This script creates the structure for a new bash manager based app. 5 | # 6 | # 7 | # It will create the CODE directory for you. 8 | # The CODE directory contains two things: 9 | # 10 | # - optionally the bash_manager_core-1.0.sh script (or a link to the original bash_manager_core-1.0.sh script) 11 | # - the HOME directory: 12 | # 13 | # The home directory has the following structure: 14 | # - home: 15 | # ----- config.defaults 16 | # ----- config.d 17 | # ----- tasks.d 18 | # 19 | # Options: 20 | # -c CODEPath The path to the CODE directory. 21 | # It will be created if it doesn't exist. 22 | # This option is mandatory. 23 | # 24 | # -i imports the bash manager script into the CODE directory. 25 | # -l (lowercase L) if the bash manager script is imported, use a symbolic link rather than a copy. 26 | # 27 | # 28 | 29 | 30 | codePath="" 31 | importBashManager=0 32 | useLink=0 33 | bashManagerVersion="1.0" 34 | 35 | 36 | while getopts :c:il opt; do 37 | case "$opt" in 38 | c) codePath="$OPTARG" ;; 39 | i) importBashManager=1 ;; 40 | l) useLink=1 ;; 41 | esac 42 | done 43 | 44 | 45 | abort() 46 | { 47 | echo "new_app: abort: $1" 48 | exit 1 49 | } 50 | 51 | 52 | # First let's check that we are inside the new_app.sh directory 53 | #if ! [ "./new_app.sh" = "$0" ]; then 54 | # abort "You need to be inside the directory that contains new_app.sh to call it!" 55 | #fi 56 | 57 | 58 | 59 | # Then the codePath must be specified 60 | if [ -z "$codePath" ]; then 61 | abort "You must specify the CODE path with the -c option" 62 | fi 63 | 64 | 65 | 66 | # Let's try to create the CODE directory first (see if we don't have perms problems) 67 | mkdir -p "$codePath" 68 | if ! [ 0 -eq $? ]; then 69 | abort "Could not create the CODE directory ($codePath)" 70 | fi 71 | 72 | 73 | 74 | # One last thing: if the bash manager script is to be imported, 75 | # we need to find the path to the bash manager script 76 | if [ 1 -eq $importBashManager ]; then 77 | cd $(dirname "$0") 78 | bashManagerProgramName="bash_manager_core-${bashManagerVersion}.sh" 79 | bashManagerPath="$(pwd)/../$bashManagerProgramName" 80 | 81 | if ! [ -f "$bashManagerPath" ]; then 82 | abort "Bash manager script not found: $bashManagerPath" 83 | fi 84 | 85 | fi 86 | 87 | # If we have made it so far, creating the basic structure should be straight forward 88 | cd "$codePath" 89 | mkdir -p home/config.d home/tasks.d 90 | touch home/config.defaults 91 | 92 | 93 | if [ 1 -eq $importBashManager ]; then 94 | if [ 1 -eq $useLink ]; then 95 | if [ -f "$bashManagerProgramName" ]; then 96 | rm "$bashManagerProgramName" 97 | fi 98 | ln -s "$bashManagerPath" "$bashManagerProgramName" 99 | else 100 | cp "$bashManagerPath" . 101 | fi 102 | fi 103 | 104 | 105 | echo "new_app: done" 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /code/utils/new_home.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | # This script creates the structure for a new home 5 | # 6 | # 7 | # It will create the HOME directory for you. 8 | # The home directory has the following structure: 9 | # - HOME: 10 | # ----- config.defaults 11 | # ----- config.d 12 | # ----- tasks.d 13 | # 14 | # Options: 15 | # -h homePath The path to the HOME directory to create 16 | # -f fill the structure with some hello like examples 17 | # 18 | # 19 | 20 | 21 | homePath="" 22 | feed=0 23 | 24 | 25 | while getopts :h:f opt; do 26 | case "$opt" in 27 | f) feed=1 ;; 28 | h) homePath="$OPTARG" ;; 29 | esac 30 | done 31 | 32 | 33 | abort() 34 | { 35 | echo "new_home: abort: $1" 36 | exit 1 37 | } 38 | 39 | 40 | # Check that homePath is specified 41 | if [ -z "$homePath" ]; then 42 | abort "You must specify the HOME path with the -h option" 43 | fi 44 | 45 | 46 | 47 | # Let's try to create the HOME directory first (see if we don't have perms problems) 48 | mkdir -p "$homePath" 49 | if ! [ 0 -eq $? ]; then 50 | abort "Could not create the HOME directory ($homePath)" 51 | fi 52 | 53 | 54 | # If we have made it so far, creating the basic structure should be straight forward 55 | cd "$homePath" 56 | mkdir -p config.d tasks.d 57 | touch config.defaults 58 | 59 | 60 | if [ 1 -eq $feed ]; then 61 | 62 | 63 | cat > tasks.d/hello.sh <<"DELIM" 64 | #!/bin/bash 65 | 66 | 67 | startTask "Hello" 68 | echo "Hello $VALUE" 69 | endTask "Hello" 70 | 71 | DELIM 72 | 73 | cat > config.d/me.txt <<"DELIM" 74 | hello: 75 | project1=World! 76 | 77 | DELIM 78 | 79 | fi 80 | 81 | echo "new_home: done" 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /doc/aliases.eng.md: -------------------------------------------------------------------------------- 1 | Being more productive: creating command line alias 2 | ================================================== 3 | 2015-10-13 4 | 5 | 6 | 7 | 8 | The bash manager command line can become wordy quite rapidly. 9 | A solution for keeping a concise command line is to use aliases. 10 | 11 | 12 | 13 | Locations 14 | ------------- 15 | 16 | Aliases are stored in files which reside in the following locations: 17 | 18 | - ~/.bash_manager 19 | - /etc/.bash_manager 20 | 21 | 22 | The first file found only is used, meaning that if you have created the two mentioned aliases files, 23 | the ~/.bash_manager file has precedence over the /etc/.bash_manager file. 24 | 25 | 26 | 27 | The alias file structure 28 | ------------------------------ 29 | 30 | The abstract notation would be: 31 | 32 | 33 | ``` 34 | chanel[programName]: 35 | 36 | alias = aliasValue 37 | alias2 = aliasValue2 38 | ... 39 | 40 | chanel2[programName2]: 41 | 42 | alias = aliasValue 43 | alias2 = aliasValue2 44 | ... 45 | ``` 46 | 47 | 48 | Indentation is not necessary, I added it for the sake of readability. 49 | Also, spaces around the spaces symbol are not required. 50 | 51 | 52 | chanel is equal to alias. 53 | It might take other values in the future, that's because a .bash_manager is a general purpose 54 | config file for the bash manager, it's not JUST an alias file. 55 | 56 | 57 | The programName is the name of your program. 58 | Remember that you can change your program in different ways, the most common being to set it in the config.defaults file, 59 | see the 60 | [task author cheatsheet](https://github.com/lingtalfi/bashmanager/blob/master/doc/task-author-cheatsheet.eng.md) 61 | for more info. 62 | 63 | 64 | 65 | Usage example 66 | ------------------ 67 | 68 | Imagine I create a ~/.bash_manager file with the following content: 69 | 70 | ``` 71 | alias[webWizard]: 72 | 73 | ddb = -t downloadRemoteDb -p 74 | upsync = -t upsync -p 75 | 76 | 77 | ``` 78 | 79 | 80 | Then, instead of typing this: 81 | 82 | 83 | ```bash 84 | wwiz -t downloadRemoteDb -p sketch 85 | ``` 86 | 87 | (wwiz is my alias for a web wizard tool I'm working on now) 88 | 89 | I could type this: 90 | 91 | ```bash 92 | wwiz ddb sketch 93 | ``` 94 | 95 | 96 | Which is way much better, and glorifies the conciseness of the command line, I hope you will agree on this. 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /doc/commandLineOptions.eng.md: -------------------------------------------------------------------------------- 1 | Command line options 2 | ================================================== 3 | 2015-10-14 4 | 5 | 6 | 7 | 8 | Any bash manager software inherits the following command line options. 9 | 10 | 11 | 12 | 13 | -c configPath: 14 | 15 | adds a user config file to parse. 16 | It can be used multiple times. 17 | configPath is a relative path, relative to HOME/config.d directory. 18 | The .txt extension must not be specified. 19 | If the -c option is not set, all config files located in config.d will be used (this is probably not what you want). 20 | 21 | 22 | -h home: 23 | 24 | sets the HOME path. 25 | 26 | 27 | -p project: 28 | 29 | adds a project to parse. 30 | It can be used multiple times. 31 | 32 | By default, if you don't specify a project, bashmanager will execute all 33 | the projects that it finds. 34 | 35 | As of version 1.06, if you don't want to use a project, but just use a task and providing your own options for instance, 36 | you can use the special _none_ value 37 | 38 | 39 | 40 | -s: 41 | 42 | set the STRICT_MODE flag to 1. 43 | If STRICT_MODE=1, your software will quit at the first error encountered. 44 | 45 | -t task: 46 | 47 | adds a task to parse. 48 | It can be used multiple times. 49 | 50 | -v: 51 | 52 | set the VERBOSE flag to 1. 53 | If VERBOSE=0, the log and warning calls do nothing. 54 | If VERBOSE=1, the log and warning calls output to STDOUT. 55 | 56 | -vv: 57 | 58 | set the VERBOSE flag to 2. 59 | At this level, the bash manager core script details what it is doing. 60 | 61 | 62 | --option-key=value: 63 | 64 | 65 | set an entry in the CONFIG array, with the given key and value. 66 | 67 | Can also be used to override a specific task's value, globally, or for a project in particular. 68 | To override a task's value, your key must have the following format: 69 | 70 | key: <_VALUE_> <:project>? 71 | 72 | For instance if I want to set the value of a task named depositories to /tmp/mydepo and for all projects, 73 | you can use the following option: 74 | 75 | --option-_VALUE_depositories=/tmp/mydep 76 | 77 | Now if you want to restrict this assignment to the project martin only, you can use the following: 78 | 79 | --option-_VALUE_depositories:martin=/tmp/mydep 80 | 81 | --option-key value: 82 | 83 | As of version 1.07 84 | Same as --option-key=value 85 | 86 | -------------------------------------------------------------------------------- /doc/constraints.eng.md: -------------------------------------------------------------------------------- 1 | Constraints 2 | ================================================== 3 | 2015-10-16 4 | 5 | 6 | 7 | 8 | 9 | 10 | Recommendations 11 | ------------------- 12 | 13 | 14 | ### Tasks should have unique base names 15 | 16 | As of 1.06, a bug has been fixed that allows tasks to be searched recursively. 17 | So this allows the task author to organize her tasks in folders like this for instance: 18 | 19 | - tasks.d 20 | ----- database 21 | --------- applyPatch.php 22 | --------- backupRemote.php 23 | ----- image 24 | --------- resize.php 25 | --------- crop.php 26 | 27 | The problem that comes with this feature is that now two file can possibly have the same 28 | [baseName](https://github.com/lingtalfi/ConventionGuy/blob/master/nomenclature.fileName.eng.md). 29 | When this occurs, bash manager only considers the first instance found. 30 | Therefore, we recommend that task authors don't use identical basename for their task files. 31 | 32 | 33 | -------------------------------------------------------------------------------- /doc/foreign-script-guidelines.eng.md: -------------------------------------------------------------------------------- 1 | Foreign scripting guidelines 2 | ================================ 3 | 2015-10-13 4 | 5 | 6 | 7 | By default, tasks in bash manager are coded with the bash scripting language. 8 | However, because some languages are sometimes more appropriate than others, 9 | bash manager also handles the following scripting languages: php, python, ruby and perl. 10 | 11 | Those are called foreign scripts in the bash manager lingo. 12 | 13 | 14 | 15 | In order to use foreign script at their full potential, foreign script authors should be aware of the special rules described below. 16 | 17 | 18 | 19 | Printing a line 20 | -------------------- 21 | 22 | A line printed inside a foreign script is printed as is by the Bash manager script. 23 | The only exceptions are the special notations described in this document. 24 | 25 | All the lines that you print from a foreign script should end with the "end of line" symbol (\n), 26 | that's because the bash manager does some post-processing on them. 27 | 28 | 29 | Reading values from the CONFIG and OTHER_VALUES arrays 30 | ------------------------------------------ 31 | 32 | 33 | In a foreign script, the CONFIG array is accessible for reading via the foreign script's environment. 34 | 35 | Each foreign script has its own way to access the environment. 36 | In php for instance, the environment variables are stored in the $_SERVER variable. 37 | 38 | When exported to the foreign script environment, the keys of the CONFIG array are converted to uppercase. 39 | So for instance, if your CONFIG array contains a key foo, you have to access it from your foreign script 40 | environment using the key BASH_MANAGER_CONFIG_FOO. 41 | 42 | You should know that the CONFIG array contains a special key _VALUE representing the current task's value. 43 | You can access it with the key: BASH_MANAGER_CONFIG__VALUE. 44 | 45 | Similarly, a foreign script can read the values of the OTHER_VALUES array. 46 | For instance, if the OTHER_VALUES array contains a key task1, 47 | the foreign script can access to the corresponding value using the key following key from its environment: 48 | 49 | ```bash 50 | BASH_MANAGER_OTHER_VALUES_TASK1 51 | ``` 52 | 53 | 54 | 55 | Writing values in the CONFIG 56 | -------------------------------- 57 | 58 | The CONFIG array can be used to share data between the different tasks (scripts). 59 | In order to set a key FOO with value 789 in the CONFIG array, a foreign script needs to write the following 60 | special line: 61 | 62 | ```bash 63 | BASH_MANAGER_CONFIG_FOO=789 64 | ``` 65 | 66 | In php, that would be: 67 | 68 | ```php 69 | <:> 99 | ``` 100 | 101 | For instance, if you want to use the native log function with message hello, you can print the following line 102 | 103 | 104 | ```bash 105 | log: hello 106 | ``` 107 | 108 | In php for instance, that would look like this: 109 | 110 | 111 | ```php 112 | echo "log: hello" . PHP_EOL; // this will call the native log function defined in the bash manager core script. 113 | ``` 114 | 115 | Don't forget to end the line with the end of line symbol, or bash manager might not be able to process it properly. 116 | 117 | 118 | 119 | 120 | Some snippets 121 | -------------------- 122 | 123 | ### java 124 | 125 | ``` 126 | # that's the call in your bash manager configuration 127 | MyBashManHello(java): 128 | project1=Friday 129 | ``` 130 | 131 | ```java 132 | # source code of your java program 133 | public class MyBashManHello { 134 | 135 | 136 | public static void main(String[] args) { 137 | 138 | String value = System.getenv("BASH_MANAGER_CONFIG__VALUE"); 139 | System.out.println("log:Java said: value from bash manager is " + value); 140 | 141 | } 142 | } 143 | 144 | ``` 145 | 146 | Note: we don't need to bother with the package, because bash manager simply don't handles them. 147 | So instead, we use a program name that will not conflict with our other java programs. 148 | Here: MyBashManHello does not conflict with any other java program in my environment. 149 | 150 | 151 | -------------------------------------------------------------------------------- /doc/images/bash-manager-structure.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingtalfi/bashmanager/272db7908773c4ae217270d1c67f6e6fc4ba6d11/doc/images/bash-manager-structure.jpg -------------------------------------------------------------------------------- /doc/install-bashmanager.eng.md: -------------------------------------------------------------------------------- 1 | Install bashmanager 2 | ========================= 3 | 2015-10-15 4 | 5 | 6 | 7 | This document describes the procedure to install the bash manager command on your machine, 8 | and make it system wide available as the "bashman" command. 9 | 10 | 11 | There are two steps: 12 | 13 | - download the code 14 | - create the bashman command 15 | 16 | 17 | 18 | 19 | Download the code 20 | ------------------------------- 21 | 22 | Download the basmanager code (or clone it) and put it in a directory of your choice. 23 | 24 | 25 | 26 | Create the bashman command 27 | ------------------------------- 28 | 29 | 30 | To make the bashman command system wide available, it needs to be in one of the directories listed in your PATH. 31 | Assuming that /usr/local/bin is in your PATH, we will create a bashman symbolic link that points to 32 | the actual bash manager command, which is in the code directory. 33 | 34 | ```bash 35 | > ln -s /path/to/real/bashmanager/code/bashbash_manager.sh /usr/local/bin/bashman 36 | ``` 37 | 38 | 39 | Voilà. 40 | Check your work: 41 | 42 | ```bash 43 | > which bashman 44 | /usr/local/bin/bashman 45 | ``` 46 | -------------------------------------------------------------------------------- /doc/task-author-cheatsheet.eng.md: -------------------------------------------------------------------------------- 1 | Task Author Cheatsheet 2 | =========================== 3 | 2015-09-21 4 | 5 | 6 | 7 | 8 | Bash manager task author's cheatsheet. 9 | 10 | 11 | functions: 12 | 13 | - error ( message ) : sends a message to STDERR, exit only if STRICT mode is on (-s option) 14 | - log ( message ): sends a message to STDOUT, only if VERBOSE mode is on (-v option) 15 | - warning ( message ): like log, but the text color is orange 16 | - startTask ( taskName ): uses log to display a visual start separator 17 | - endTask ( taskName ): uses log to display a visual end separator 18 | 19 | 20 | variables: 21 | 22 | - VALUE 23 | 24 | string: contains the task's value for the project. Can be empty. 25 | 26 | 27 | - OTHER_VALUE 28 | 29 | associative array: contains the other task's values for the current project. 30 | 31 | OTHER_VALUE[taskName]=value 32 | 33 | - CONFIG 34 | 35 | associative array: contains the key/value pairs from HOME/config.defaults, plus 36 | the options set via the command line (--option-key=value). 37 | Also contains some special values added by the bash manager: 38 | 39 | CONFIG[_HOME]: path to the application's home 40 | CONFIG[_VALUE]: the current task's value for the current project 41 | CONFIG[_CONFIG_FILE]: the current config file (path relative to the config directory) 42 | 43 | To override the task's value from the command line, you can use the --option-key=value format, 44 | but your key must have the following format: 45 | 46 | key: <_VALUE_> <:project>? 47 | 48 | For instance if I want to set the value of a task named depositories to /tmp/mydepo and for all projects, 49 | you can use the following option: 50 | 51 | --option-_VALUE_depositories=/tmp/mydep 52 | 53 | Now if you want to restrict this assignment to the project martin only, you can use the following: 54 | 55 | --option-_VALUE_depositories:martin=/tmp/mydep 56 | 57 | 58 | 59 | 60 | - CONFIG_OPTIONS 61 | 62 | associative array: contains only the options set via the command line with the format --option-key=value 63 | 64 | 65 | 66 | config.defaults 67 | -------------------- 68 | 69 | ### Set the program name 70 | 71 | ``` 72 | _program_name=myProgram 73 | ``` 74 | -------------------------------------------------------------------------------- /doc/task-author-guidelines.eng.md: -------------------------------------------------------------------------------- 1 | Task Author Guidelines 2 | =========================== 3 | 2015-09-21 4 | 5 | 6 | 7 | 8 | In this document, we discuss the basic guidelines for writing tasks.
9 | Those guidelines are just suggestions for task authors, they are conventions rather than strict rules. 10 | 11 | 12 | 13 | 14 | Wrap your code with startTask and endTask 15 | --------------------------------------------- 16 | 17 | Visual coherence is appreciable. 18 | 19 | When you are running your program in the interactive shell, and the debug mode is on, 20 | it's always nice to have some clean and well organized output. 21 | 22 | The startTask and endTask functions help doing that.
23 | They provide visual separators indicating when your task starts and ends. 24 | 25 | Here is a sample code: 26 | 27 | ```bash 28 | 29 | startTask myTask 30 | 31 | # your code here 32 | 33 | endTask myTask 34 | 35 | 36 | ``` 37 | 38 | 39 | 40 | 41 | Use the error, log and warning functions 42 | --------------------------------------------- 43 | 44 | For error handling, consider using the error, log and warning functions.
45 | Those 3 core functions are all what you need to handle the errors and inform the interactive user of what's going on.
46 | Here is how and when you should use them: 47 | 48 | - error: 49 | 50 | should be used when a task's author expectation is not met. 51 | For instance, if a variable is supposed to be initialized, but is actually not initialized, then 52 | it's a case where using the error function is appropriate. 53 | The error function is a way to indicate that a task could not complete as expected. 54 | 55 | The error message goes to STD_ERR. 56 | 57 | void error ( msg ) 58 | 59 | 60 | 61 | - log: 62 | 63 | it displays a message to STD_OUT. 64 | It's meant to help debugging while working with the interactive console. 65 | Write the important expected things that your task is supposed to do. 66 | 67 | 68 | void log ( msg ) 69 | 70 | - warning: 71 | 72 | like the log function, but is used to state a non optimal condition. 73 | 74 | For instance, imagine that you write a software that creates an ID card. 75 | It takes the identity of the user and prints its to the output. 76 | The task needs two parameters from the user: the first name and the last name. 77 | If the first name is empty (the user left it blank), you could use a warning. 78 | 79 | Warning are never critical, they won't stop the task. 80 | 81 | 82 | void warning ( msg ) 83 | 84 | 85 | By convention, the message parameter passed to the error, log and warning functions should start with the task's name 86 | followed by a colon and a space, like this: 87 | 88 | ```bash 89 | # using the log function 90 | log "myTask: secret file transferred." 91 | 92 | # using the warning function 93 | log "myTask: the user's first name is empty!" 94 | 95 | # using the error function 96 | log "myTask: file not found: ${filePath}" 97 | ``` 98 | 99 | 100 | 101 | 102 | Do not exit 103 | ------------------ 104 | 105 | Never put an exit statement in your bash task, this would prevent other tasks from being executed. 106 | If you use a foreign script, you can safely exit, because foreign scripts are called in their own processes. 107 | 108 | 109 | 110 | 111 | 112 | Handle the case of the empty task value 113 | ------------------------------------------ 114 | 115 | Some task don't require value, they just need to be called. 116 | Some other tasks require a (per project) task value and cannot work without it. 117 | 118 | As of version 1.06, it is possible to execute a task without specifying a project, 119 | which basically means that any task can be called without a value specified. 120 | 121 | Therefore, task authors should always handle the case where an empty value is passed to their task. 122 | In a task where the value is required, this would probably mean skip the task without triggering a warning or an error. 123 | 124 | 125 | 126 | Php task authors, use the phpManager plugin 127 | ------------------------------------------------ 128 | 129 | If you are comfortable with php, you should look at the 130 | [phpManager plugin](https://github.com/lingtalfi/bashmanager_plugin_phpmanager), 131 | which make developing tasks with php easier. 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /doc/task-author-reserved-functions.eng.md: -------------------------------------------------------------------------------- 1 | Task Author Reserved Functions 2 | ==================================== 3 | 2015-09-21 4 | 5 | 6 | 7 | 8 | The following functions are used in the core, and therefore you shouldn't define them in your tasks. 9 | 10 | 11 | 12 | - _newFileInitCpt 13 | - _newFileName 14 | - abort 15 | - collectConfig 16 | - confError 17 | - createExpandedCommandLine 18 | - dumpAssoc 19 | - endTask 20 | - error 21 | - exportVars 22 | - inArray 23 | - log 24 | - logg 25 | - newFileName 26 | - parseAliases 27 | - parseAllValues 28 | - printAssocArray 29 | - printConfigLines 30 | - printCurrentTime 31 | - printDate 32 | - printList 33 | - printRealTaskExtension 34 | - printStack 35 | - printStackOrList 36 | - printTrace 37 | - processCommandLine 38 | - processHomeFromCommandLine 39 | - processScriptOutput 40 | - splitLine 41 | - startTask 42 | - strPos 43 | - strRepeat 44 | - toList 45 | - toStack 46 | - warning -------------------------------------------------------------------------------- /the-flexible-software-tutorial.md: -------------------------------------------------------------------------------- 1 | The Flexible Software Tutorial 2 | ==================================== 3 | 2015-09-21 4 | 5 | 6 | The goal of this tutorial is to demonstrate the flexibility that any software created with bash manager automatically 7 | inherits. 8 | 9 | This is an easy to follow tutorial, using the "copy paste observe" technique. 10 | I assume that you have already completed at least the hello world tutorial from the [main documentation] (https://github.com/lingtalfi/bashmanager). 11 | 12 | 13 | 14 | The flexibility of any Bash manager software comes from the fact that the tasks that compose your software can be assembled in 15 | many different ways, and using two mediums: the config file and the command line options. 16 | 17 | In this tutorial, we will test and observe such power by invoking the software from the command line. 18 | But before we can do that, we need to prepare the software first. 19 | 20 | 21 | 22 | Creating the software 23 | --------------------------- 24 | 25 | The software we will be creating is totally abstract. 26 | We will use dumb (boring) names so that you can better see the flexibility of the software (which is the goal of this tutorial). 27 | 28 | Let's create 5 tasks to play with. 29 | 30 | Create the following files (or download them from the code/tutorials/the-flexible-tutorial directory): 31 | 32 | - HOME/tasks.d/t1.sh 33 | - HOME/tasks.d/t2.sh 34 | - HOME/tasks.d/t3.sh 35 | - HOME/tasks.d/t4.sh 36 | - HOME/tasks.d/t5.sh 37 | 38 | 39 | And put the following content in them: 40 | 41 | 42 | ```bash` 43 | 44 | echo "$task: processing value $VALUE (project $project)" 45 | 46 | ```` 47 | 48 | 49 | Note: although I used them for this tutorial, I would not recommend to use the **non documented** $task and $project variables, unless you know where they come from. 50 | 51 | 52 | 53 | Ok, so that was the hard part. 54 | Now our software is created, we need to configure it. 55 | 56 | 57 | 58 | Configuring the software 59 | ----------------------------- 60 | 61 | I will assume that we have 3 clients, each client correspond to a project, and for each client we want to prepare a profile 62 | in the configuration file. 63 | 64 | 65 | Create the HOME/config.d/me.txt and put the following content in it: 66 | 67 | 68 | t1: 69 | p1=123 70 | p2=456 71 | p3=789 72 | 73 | 74 | t2: 75 | p1=apple 76 | p3=cherry 77 | 78 | 79 | t3: 80 | p2=karate 81 | p3= 82 | 83 | 84 | t4: 85 | p1=square 86 | p2=oval 87 | 88 | 89 | t5: 90 | p1=red 91 | p2=green 92 | p3=blue 93 | 94 | 95 | 96 | 97 | Now please take some time and try to guess what this configuration file does. 98 | 99 | ``` 100 | ... 101 | ``` 102 | 103 | 104 | 105 | The configuration file defines 3 profiles, one per client. 106 | Basically, this config file defines the default values that your software 107 | will be using for each client. 108 | 109 | 110 | Don't forget the config.defaults 111 | ----------------------------- 112 | 113 | Finally create an empty HOME/config.defaults file. 114 | It's required by the software (otherwise it complains). 115 | We won't be using that file in this tutorial so don't bother about it. 116 | 117 | 118 | 119 | Now, our software is fully functional, let's use it! 120 | 121 | 122 | 123 | Using the software 124 | ----------------------------- 125 | 126 | We first need to define the location of the HOME that we just created. 127 | The HOME is actually what characterizes the software, and the bash_manager.sh script is just 128 | the executive part of it. 129 | 130 | The cool thing is that you can reuse the same bash manager script to execute any HOME, 131 | and therefore focus on the task development (the hard part, remember?). 132 | 133 | 134 | The best way to use a software is to create a specialized command, a wrapper, that would contain the 135 | absolute path to our HOME. Then we put this command in the PATH of our system and voilà! We can 136 | use our command on that host machine. 137 | 138 | Well, that was just a suggestion that you should seriously consider if you write software that 139 | will be used by others. 140 | 141 | But for now, we will simply define the home location from the command line. 142 | Our commands will be more verbose, but at least we can get started without further ado. 143 | 144 | To define the home location from the command line, we need to use the -h switch.
145 | We also need to define the config file to use with the -c switch.
146 | This is actually **VERY VERY IMPORTANT** because otherwise, bash manager would execute all config files 147 | found in the HOME/config.d directory (which is almost never what you want). 148 | 149 | Let's try it and see what happens (of course, replace the paths by your own paths): 150 | 151 | 152 | ```bash 153 | cd /path/to/bash_managers_parent_directory 154 | ./bash_manager.sh -h /path/to/our_softwares_home -c me 155 | ``` 156 | 157 | 158 | If you do it right, the output will be: 159 | 160 | 161 | t1: processing value 123 (project p1) 162 | t2: processing value apple (project p1) 163 | t4: processing value square (project p1) 164 | t5: processing value red (project p1) 165 | t1: processing value 456 (project p2) 166 | t3: processing value karate (project p2) 167 | t4: processing value oval (project p2) 168 | t5: processing value green (project p2) 169 | t1: processing value 789 (project p3) 170 | t2: processing value cherry (project p3) 171 | t3: processing value (project p3) 172 | t5: processing value blue (project p3) 173 | 174 | 175 | 176 | Since this is a long command to type already, type the following (unless you already have a bashman wrapper to the bash_manager.sh script): 177 | 178 | ```bash 179 | alias bashman='./bash_manager.sh -h /path/to/our_softwares_home -c me' 180 | ``` 181 | 182 | 183 | Verify that you get the same output: 184 | 185 | ```bash 186 | bashman 187 | ``` 188 | 189 | Output: 190 | 191 | t1: processing value 123 (project p1) 192 | t2: processing value apple (project p1) 193 | t4: processing value square (project p1) 194 | t5: processing value red (project p1) 195 | t1: processing value 456 (project p2) 196 | t3: processing value karate (project p2) 197 | t4: processing value oval (project p2) 198 | t5: processing value green (project p2) 199 | t1: processing value 789 (project p3) 200 | t2: processing value cherry (project p3) 201 | t3: processing value (project p3) 202 | t5: processing value blue (project p3) 203 | 204 | 205 | 206 | There are many things to observe: 207 | 208 | - the projects are executed in order: first p1, then p2, and then p3 209 | - the tasks, in the scope of a project, are also executed in order of appearance in the HOME/config.d/me.txt config file 210 | - in our config file, we didn't set a assign project p2 to task t2, and indeed task t2 hasn't been executed for project p2 211 | - in our config file, we assigned project p3 with an empty value to task t3, and indeed task t3 has been executed with an empty value for project p3 212 | 213 | 214 | But the most important thing is to understand that by default, the bash manager script executes 215 | every project and every task it finds in the config file. 216 | 217 | 218 | 219 | 220 | Now, let's leverage the command line and see how we can customize the software to our needs. 221 | I will use a **recipe approach** for the rest of this tutorial. 222 | 223 | 224 | 225 | #### Execute only task t1 of every project 226 | 227 | ```bash 228 | bashman -t t1 229 | ``` 230 | 231 | Output: 232 | 233 | t1: processing value 123 (project p1) 234 | t1: processing value 456 (project p2) 235 | t1: processing value 789 (project p3) 236 | 237 | 238 | 239 | 240 | #### Execute only task t1 and task t3 of every project 241 | 242 | ```bash 243 | bashman -t t1 -t t3 244 | ``` 245 | 246 | Output: 247 | 248 | t1: processing value 123 (project p1) 249 | t1: processing value 456 (project p2) 250 | t3: processing value karate (project p2) 251 | t1: processing value 789 (project p3) 252 | t3: processing value (project p3) 253 | 254 | 255 | #### Execute only project p1 256 | 257 | ```bash 258 | bashman -p p1 259 | ``` 260 | 261 | Output: 262 | 263 | t1: processing value 123 (project p1) 264 | t2: processing value apple (project p1) 265 | t4: processing value square (project p1) 266 | t5: processing value red (project p1) 267 | 268 | 269 | 270 | #### Execute only project p1 and project p2 271 | 272 | ```bash 273 | bashman -p p1 -p p2 274 | ``` 275 | 276 | Output: 277 | 278 | t1: processing value 123 (project p1) 279 | t2: processing value apple (project p1) 280 | t4: processing value square (project p1) 281 | t5: processing value red (project p1) 282 | t1: processing value 456 (project p2) 283 | t3: processing value karate (project p2) 284 | t4: processing value oval (project p2) 285 | t5: processing value green (project p2) 286 | 287 | 288 | 289 | #### Execute only task t1 for project p1 and project p2 290 | 291 | ```bash 292 | bashman -p p1 -p p2 -t t1 293 | ``` 294 | 295 | Output: 296 | 297 | t1: processing value 123 (project p1) 298 | t1: processing value 456 (project p2) 299 | 300 | 301 | 302 | #### Execute only task t1 for project p1 and project p2, and change the value of task t1 to joker 303 | 304 | ```bash 305 | bashman -p p1 -p p2 -t t1 --option-_VALUE_t1=joker 306 | ``` 307 | 308 | Output: 309 | 310 | t1: processing value joker (project p1) 311 | t1: processing value joker (project p2) 312 | 313 | 314 | 315 | #### Execute only task t1 for project p1 and project p2, and change the value of task t1 to joker for project p1, and to rekoj for project p2 (woa, that's a damn long title) 316 | 317 | ```bash 318 | bashman -p p1 -p p2 -t t1 --option-_VALUE_t1:p1=joker --option-_VALUE_t1:p2=rekoj 319 | ``` 320 | 321 | Output: 322 | 323 | t1: processing value joker (project p1) 324 | t1: processing value rekoj (project p2) 325 | 326 | 327 | 328 | 329 | If you realize that every software written with bash_manager inherits those command line options, and thus this flexibility, 330 | then you have successfully completed this tutorial, congratulations! 331 | 332 | 333 | 334 | 335 | 336 | 337 | --------------------------------------------------------------------------------