diff --git a/.gitignore b/.gitignore index a17b6e82..8e6f12a1 100755 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,8 @@ bats enabled/* /enabled tmp/ + +# Do not save profiles +profiles/* +# apart from the default one +!profiles/default.bash_it diff --git a/completion/available/bash-it.completion.bash b/completion/available/bash-it.completion.bash index 4fdd72d6..18cd241a 100644 --- a/completion/available/bash-it.completion.bash +++ b/completion/available/bash-it.completion.bash @@ -57,6 +57,18 @@ _bash-it-comp-list-available() COMPREPLY=( $(compgen -W "${enabled_things}" -- ${cur}) ) } +_bash-it-comp-list-profiles() +{ + local profiles + + profiles=$(for f in `compgen -G "${BASH_IT}/profiles/*.bash_it" | sort -d`; + do + basename $f | sed -e 's/.bash_it//g' + done) + + COMPREPLY=( $(compgen -W "${profiles}" -- ${cur}) ) +} + _bash-it-comp() { local cur prev opts @@ -65,7 +77,7 @@ _bash-it-comp() prev="${COMP_WORDS[COMP_CWORD-1]}" chose_opt="${COMP_WORDS[1]}" file_type="${COMP_WORDS[2]}" - opts="disable enable help migrate reload restart doctor search show update version" + opts="disable enable help migrate reload restart profile doctor search show update version" case "${chose_opt}" in show) local show_args="aliases completions plugins" @@ -82,6 +94,33 @@ _bash-it-comp() return 0 fi ;; + profile) + case "${file_type}" in + load) + if [[ "load" == "$prev" ]]; then + _bash-it-comp-list-profiles + fi + return 0 + ;; + rm) + if [[ "rm" == "$prev" ]]; then + _bash-it-comp-list-profiles + fi + return 0 + ;; + save) + return 0 + ;; + list) + return 0 + ;; + *) + local profile_args="load save list rm" + COMPREPLY=( $(compgen -W "${profile_args}" -- ${cur}) ) + return 0 + ;; + esac + ;; doctor) local doctor_args="errors warnings all" COMPREPLY=( $(compgen -W "${doctor_args}" -- ${cur}) ) diff --git a/docs/commands/index.rst b/docs/commands/index.rst index 3eee3b3a..3890a139 100644 --- a/docs/commands/index.rst +++ b/docs/commands/index.rst @@ -13,3 +13,4 @@ You should be familiar with them in order to fully utilize Bash-it. search reload doctor + profile diff --git a/docs/commands/profile.rst b/docs/commands/profile.rst new file mode 100644 index 00000000..67ca9b5b --- /dev/null +++ b/docs/commands/profile.rst @@ -0,0 +1,31 @@ +.. _profile: + +Bash-it Profile +--------------- + +Have you ever wanted to port your *Bash-it* configuration into another machine? + +If you did, then ``bash-it profile`` is for you! + +This command can save and load custom *"profile"* files, that can be later +used to load and recreate your configuration, in any machine you would like |:smile:| + +When porting your configuration into a new machine, you just need to save your current profile, copy the resulting *"profile"* file, and load it in the other machine. + +Example +^^^^^^^ + +.. code-block:: bash + + # Saves your current profile + bash-it profile save my_profile + # Load the default profile, which is the one used in the default installation. + bash-it profile load default + + # Do whatever you want: + # Disable stuff + bash-it disable ... + # Enable stuff + bash-it enable ... + # If you want to get back into your original configuration, you can do it easily + bash-it profile load my_profile diff --git a/install.sh b/install.sh index 5d2f883e..2bb78a3f 100755 --- a/install.sh +++ b/install.sh @@ -208,6 +208,8 @@ export BASH_IT_AUTOMATIC_RELOAD_AFTER_CONFIG_CHANGE='' source "${BASH_IT}"/vendor/github.com/erichs/composure/composure.sh # shellcheck source=./lib/utilities.bash source "$BASH_IT/lib/utilities.bash" +# shellcheck source=./lib/log.bash +source "${BASH_IT}/lib/log.bash" cite _about _param _example _group _author _version # shellcheck source=./lib/helpers.bash source "$BASH_IT/lib/helpers.bash" @@ -219,12 +221,7 @@ if [[ -n $interactive && -z "${silent}" ]]; then done else echo "" - echo -e "\033[0;32mEnabling reasonable defaults\033[0m" - _enable-completion bash-it - _enable-completion system - _enable-plugin base - _enable-plugin alias-completion - _enable-alias general + _bash-it-profile-load "default" fi echo "" diff --git a/lib/helpers.bash b/lib/helpers.bash index f11df4be..01211079 100755 --- a/lib/helpers.bash +++ b/lib/helpers.bash @@ -108,6 +108,7 @@ bash-it () example '$ bash-it version' example '$ bash-it reload' example '$ bash-it restart' + example '$ bash-it profile list|save|load|rm [profile_name]' example '$ bash-it doctor errors|warnings|all' typeset verb=${1:-} shift @@ -126,6 +127,8 @@ bash-it () func=_help-$component;; doctor) func=_bash-it-doctor-$component;; + profile) + func=_bash-it-profile-$component;; search) _bash-it-search $component "$@" return;; @@ -457,6 +460,172 @@ _bash-it-doctor-() { _bash-it-doctor-all } +_bash-it-profile-save() { + _about 'saves the current configuration to the "profile" directory' + _group 'lib' + + local name=$1 + while [ -z "$1" ]; do + read -r -e -p "Please enter the name of the profile to save: " name + case $name in + "") + echo -e "\033[91mPlease choose a name.\033[m" + ;; + *) + break + ;; + esac + done + + local profile_path="${BASH_IT}/profiles/${name}.bash_it" + if [ -f "$profile_path" ]; then + echo -e "\033[0;33mProfile \"$name\" already exists.\033[m" + while true; do + read -r -e -n 1 -p "Would you like to overwrite existing profile? [y/N] " RESP + case $RESP in + [yY]) + echo -e "\033[0;32mOverwriting profile \"$name\"...\033[m" + rm "$profile_path" + break + ;; + [nN] | "") + echo -e "\033[91mAborting profile save...\033[m" + return 1 + ;; + *) + echo -e "\033[91mPlease choose y or n.\033[m" + ;; + esac + done + fi + + local something_exists + echo "# This file is auto generated by Bash-it. Do not edit manually!" > "$profile_path" + for subdirectory in "plugins" "completion" "aliases"; do + local component_exists="" + echo "Saving $subdirectory configuration..." + for f in "${BASH_IT}/$subdirectory/available/"*.bash; do + _bash-it-determine-component-status-from-path "$f" + if [ "$enabled" == "x" ]; then + if [ -z "$component_exists" ]; then + # This is the first component of this type, print the header + component_exists="yes" + something_exists="yes" + echo "" >> "$profile_path" + echo "# $subdirectory" >> "$profile_path" + fi + echo "$subdirectory $enabled_file_clean" >> "$profile_path" + fi + done + done + if [ -z "$something_exists" ]; then + echo "It seems like no configuration was enabled.." + echo "Make sure to double check that this is the wanted behavior." + fi + + echo "All done!" + echo "" + echo "Profile location: $profile_path" + echo "Load the profile by invoking \"bash-it profile load $name\"" +} + +_bash-it-profile-load-parse-profile() { + _about 'Internal function used to parse the profile file' + _param '1: path to the profile file' + _param '2: dry run- only check integrity of the profile file' + _example '$ _bash-it-profile-load-parse-profile "profile.bash_it" "dry"' + + local num=0 + while read -r -a line; do + num=$((num + 1)) + # Ignore comments and empty lines + [[ -z "${line[*]}" || "${line[*]}" =~ ^#.* ]] && continue + local enable_func="_enable-${line[0]}" + local subdirectory=${line[0]} + local component=${line[1]} + + typeset to_enable=$(command ls "${BASH_IT}/$subdirectory/available/$component".*bash 2>/dev/null | head -1) + # Ignore botched lines + if [[ -z "$to_enable" ]]; then + echo -e "\033[91mBad line(#$num) in profile, aborting load...\033[m" + local bad="bad line" + break + fi + # Do not actually modify config on dry run + [[ -z $2 ]] || continue + # Actually enable the component + $enable_func "$component" + done < "$1" + + # Make sure to propagate the error + [[ -z $bad ]] +} + +_bash-it-profile-list() { + about 'lists all profiles from the "profiles" directory' + _group 'lib' + + echo "Available profiles:" + for profile in "${BASH_IT}/profiles"/*.bash_it; do + profile="${profile##*/}" + echo "${profile/.bash_it/}" + done +} + +_bash-it-profile-rm() { + about 'Removes a profile from the "profiles" directory' + _group 'lib' + local name="$1" + if [[ -z $name ]]; then + echo -e "\033[91mPlease specify profile name to remove...\033[m" + return 1 + fi + + # Users should not be allowed to delete the default profile + if [[ $name == "default" ]]; then + echo -e "\033[91mCan not remove the default profile...\033[m" + return 1 + fi + + local profile_path="${BASH_IT}/profiles/$name.bash_it" + if [[ ! -f "$profile_path" ]]; then + echo -e "\033[91mCould not find profile \"$name\"...\033[m" + return 1 + fi + + command rm "$profile_path" + echo "Removed profile \"$name\" successfully!" +} + +_bash-it-profile-load() { + _about 'loads a configuration from the "profiles" directory' + _group 'lib' + + local name="$1" + if [[ -z $name ]]; then + echo -e "\033[91mPlease specify profile name to load, not changing configuration...\033[m" + return 1 + fi + + local profile_path="${BASH_IT}/profiles/$name.bash_it" + if [[ ! -f "$profile_path" ]]; then + echo -e "\033[91mCould not find profile \"$name\", not changing configuration...\033[m" + return 1 + fi + + echo "Trying to parse profile \"$name\"..." + if _bash-it-profile-load-parse-profile "$profile_path" "dry"; then + echo "Profile \"$name\" parsed successfully!" + echo "Disabling current configuration..." + _disable-all + echo "" + echo "Enabling configuration based on profile..." + _bash-it-profile-load-parse-profile "$profile_path" + echo "" + echo "Profile \"$name\" enabled!" + fi +} + _bash-it-restart() { _about 'restarts the shell in order to fully reload it' _group 'lib' @@ -492,6 +661,25 @@ _bash-it-reload() { popd &> /dev/null || return } +_bash-it-determine-component-status-from-path () +{ + _about 'internal function used to process component name and check if its enabled' + _param '1: full path to available component file' + _example '$ _bash-it-determine-component-status-from-path "${BASH_IT}/plugins/available/git.plugin.bash' + + # Check for both the old format without the load priority, and the extended format with the priority + declare enabled_files enabled_file + enabled_file="${f##*/}" + enabled_file_clean=$(echo "$enabled_file" | sed -e 's/\(.*\)\..*\.bash/\1/g') + enabled_files=$(sort <(compgen -G "${BASH_IT}/enabled/*$BASH_IT_LOAD_PRIORITY_SEPARATOR${enabled_file}") <(compgen -G "${BASH_IT}/$subdirectory/enabled/${enabled_file}") <(compgen -G "${BASH_IT}/$subdirectory/enabled/*$BASH_IT_LOAD_PRIORITY_SEPARATOR${enabled_file}") | wc -l) + + if [ "$enabled_files" -gt 0 ]; then + enabled='x' + else + enabled=' ' + fi +} + _bash-it-describe () { _about 'summarizes available bash_it components' @@ -511,17 +699,8 @@ _bash-it-describe () printf "%-20s%-10s%s\n" "$column_header" 'Enabled?' 'Description' for f in "${BASH_IT}/$subdirectory/available/"*.bash do - # Check for both the old format without the load priority, and the extended format with the priority - declare enabled_files enabled_file - enabled_file="${f##*/}" - enabled_files=$(sort <(compgen -G "${BASH_IT}/enabled/*$BASH_IT_LOAD_PRIORITY_SEPARATOR${enabled_file}") <(compgen -G "${BASH_IT}/$subdirectory/enabled/${enabled_file}") <(compgen -G "${BASH_IT}/$subdirectory/enabled/*$BASH_IT_LOAD_PRIORITY_SEPARATOR${enabled_file}") | wc -l) - - if [ $enabled_files -gt 0 ]; then - enabled='x' - else - enabled=' ' - fi - printf "%-20s%-10s%s\n" "$(basename $f | sed -e 's/\(.*\)\..*\.bash/\1/g')" " [$enabled]" "$(cat $f | metafor about-$file_type)" + _bash-it-determine-component-status-from-path "$f" + printf "%-20s%-10s%s\n" "$enabled_file_clean" " [$enabled]" "$(cat $f | metafor about-$file_type)" done printf '\n%s\n' "to enable $preposition $file_type, do:" printf '%s\n' "$ bash-it enable $file_type <$file_type name> [$file_type name]... -or- $ bash-it enable $file_type all" @@ -540,6 +719,17 @@ _on-disable-callback() _command_exists $callback && $callback } +_disable-all () +{ + _about 'disables all bash_it components' + _example '$ _disable-all' + _group 'lib' + + _disable-plugin "all" + _disable-alias "all" + _disable-completion "all" +} + _disable-plugin () { _about 'disables bash_it plugin' @@ -623,7 +813,11 @@ _disable-thing () _bash-it-clean-component-cache "${file_type}" - printf '%s\n' "$file_entity disabled." + if [ "$file_entity" = "all" ]; then + printf '%s\n' "$file_entity $(_bash-it-pluralize-component "$file_type") disabled." + else + printf '%s\n' "$file_entity disabled." + fi } _enable-plugin () @@ -636,6 +830,12 @@ _enable-plugin () _enable-thing "plugins" "plugin" $1 $BASH_IT_LOAD_PRIORITY_DEFAULT_PLUGIN } +_enable-plugins () +{ + _about 'alias of _enable-plugin' + _enable-plugin "$@" +} + _enable-alias () { _about 'enables bash_it alias' @@ -646,6 +846,12 @@ _enable-alias () _enable-thing "aliases" "alias" $1 $BASH_IT_LOAD_PRIORITY_DEFAULT_ALIAS } +_enable-aliases () +{ + _about 'alias of _enable-alias' + _enable-alias "$@" +} + _enable-completion () { _about 'enables bash_it completion' @@ -802,6 +1008,17 @@ _help-plugins() rm $grouplist 2> /dev/null } +_help-profile () { + _about 'help message for profile command' + _group 'lib' + + echo "Manages profiles of bash it." + echo "Use 'bash-it profile list' to see all available profiles." + echo "Use 'bash-it profile save foo' to save the current configuration into a profile named 'foo'." + echo "Use 'bash-it profile load foo' to load an existing profile named 'foo'." + echo "Use 'bash-it profile rm foo' to remove an existing profile named 'foo'." +} + _help-update () { _about 'help message for update command' _group 'lib' diff --git a/profiles/default.bash_it b/profiles/default.bash_it new file mode 100644 index 00000000..5e4f4631 --- /dev/null +++ b/profiles/default.bash_it @@ -0,0 +1,12 @@ +# This is the default profile of Bash-it + +# plugins +plugins alias-completion +plugins base + +# completion +completion bash-it +completion system + +# aliases +aliases general diff --git a/test/completion/bash-it.completion.bats b/test/completion/bash-it.completion.bats index eaa4423d..fbf0a3fa 100644 --- a/test/completion/bash-it.completion.bats +++ b/test/completion/bash-it.completion.bats @@ -80,32 +80,42 @@ function __check_completion () { @test "completion bash-it: show options" { run __check_completion 'bash-it ' - assert_line -n 0 "disable enable help migrate reload restart doctor search show update version" + assert_line -n 0 "disable enable help migrate reload restart profile doctor search show update version" } @test "completion bash-it: bash-ti - show options" { run __check_completion 'bash-ti ' - assert_line -n 0 "disable enable help migrate reload restart doctor search show update version" + assert_line -n 0 "disable enable help migrate reload restart profile doctor search show update version" } @test "completion bash-it: shit - show options" { run __check_completion 'shit ' - assert_line -n 0 "disable enable help migrate reload restart doctor search show update version" + assert_line -n 0 "disable enable help migrate reload restart profile doctor search show update version" } @test "completion bash-it: bashit - show options" { run __check_completion 'bashit ' - assert_line -n 0 "disable enable help migrate reload restart doctor search show update version" + assert_line -n 0 "disable enable help migrate reload restart profile doctor search show update version" } @test "completion bash-it: batshit - show options" { run __check_completion 'batshit ' - assert_line -n 0 "disable enable help migrate reload restart doctor search show update version" + assert_line -n 0 "disable enable help migrate reload restart profile doctor search show update version" } @test "completion bash-it: bash_it - show options" { run __check_completion 'bash_it ' - assert_line -n 0 "disable enable help migrate reload restart doctor search show update version" + assert_line -n 0 "disable enable help migrate reload restart profile doctor search show update version" +} + +@test "completion bash-it: profile - show options" { + run __check_completion 'bash-it profile ' + assert_line -n 0 "load save list rm" +} + +@test "completion bash-it: profile load - show options" { + run __check_completion 'bash-it profile load ' + assert_line -n 0 "default" } @test "completion bash-it: show - show options" { diff --git a/test/fixtures/bash_it/profiles/test-bad-component.bash_it b/test/fixtures/bash_it/profiles/test-bad-component.bash_it new file mode 100644 index 00000000..8640265c --- /dev/null +++ b/test/fixtures/bash_it/profiles/test-bad-component.bash_it @@ -0,0 +1,12 @@ +# plugins +plugins alias-completion +plugins base + +# completion +completion bash-it +completion system + +# aliases +aliases general +# Bad component +aliases bla diff --git a/test/fixtures/bash_it/profiles/test-bad-type.bash_it b/test/fixtures/bash_it/profiles/test-bad-type.bash_it new file mode 100644 index 00000000..ed2d2373 --- /dev/null +++ b/test/fixtures/bash_it/profiles/test-bad-type.bash_it @@ -0,0 +1,12 @@ +# plugins +plugins alias-completion +plugins base +# Bad type +pluugins alias-completion + +# completion +completion bash-it +completion system + +# aliases +aliases general diff --git a/test/lib/helpers.bats b/test/lib/helpers.bats index 7f6664e0..d876d882 100644 --- a/test/lib/helpers.bats +++ b/test/lib/helpers.bats @@ -13,11 +13,24 @@ load ../../plugins/available/base.plugin function local_setup { setup_test_fixture + + # Copy the test fixture to the Bash-it folder + if command -v rsync &> /dev/null; then + rsync -a "$BASH_IT/test/fixtures/bash_it/" "$BASH_IT/" + else + find "$BASH_IT/test/fixtures/bash_it" \ + -mindepth 1 -maxdepth 1 \ + -exec cp -r {} "$BASH_IT/" \; + fi } # TODO Create global __is_enabled function # TODO Create global __get_base_name function # TODO Create global __get_enabled_name function +@test "bash-it: verify that the test fixture is available" { + assert_file_exist "$BASH_IT/profiles/test-bad-component.bash_it" + assert_file_exist "$BASH_IT/profiles/test-bad-type.bash_it" +} @test "helpers: _command_exists function exists" { run type -a _command_exists &> /dev/null @@ -283,6 +296,149 @@ function local_setup { assert_link_exist "$BASH_IT/enabled/225---nvm.plugin.bash" } +@test "helper: profile load command sanity" { + run _bash-it-profile-load "default" + + assert_link_exist "$BASH_IT/enabled/150---general.aliases.bash" + assert_link_exist "$BASH_IT/enabled/250---base.plugin.bash" + assert_link_exist "$BASH_IT/enabled/365---alias-completion.plugin.bash" + assert_link_exist "$BASH_IT/enabled/350---bash-it.completion.bash" + assert_link_exist "$BASH_IT/enabled/350---system.completion.bash" +} + +@test "helper: profile save command sanity" { + run _enable-plugin "nvm" + + run _bash-it-profile-save "test" + assert_line -n 0 "Saving plugins configuration..." + assert_line -n 1 "Saving completion configuration..." + assert_line -n 2 "Saving aliases configuration..." + assert_line -n 3 "All done!" + assert_file_exist "$BASH_IT/profiles/test.bash_it" +} + +@test "helper: profile save creates valid file with only plugin enabled" { + run _enable-plugin "nvm" + + run _bash-it-profile-save "test" + run cat "$BASH_IT/profiles/test.bash_it" + assert_line -n 0 "# This file is auto generated by Bash-it. Do not edit manually!" + assert_line -n 1 "# plugins" + assert_line -n 2 "plugins nvm" +} + +@test "helper: profile save creates valid file with only completion enabled" { + run _enable-completion "bash-it" + + run _bash-it-profile-save "test" + run cat "$BASH_IT/profiles/test.bash_it" + assert_line -n 0 "# This file is auto generated by Bash-it. Do not edit manually!" + assert_line -n 1 "# completion" + assert_line -n 2 "completion bash-it" +} + +@test "helper: profile save creates valid file with only aliases enabled" { + run _enable-alias "general" + + run _bash-it-profile-save "test" + run cat "$BASH_IT/profiles/test.bash_it" + assert_line -n 0 "# This file is auto generated by Bash-it. Do not edit manually!" + assert_line -n 1 "# aliases" + assert_line -n 2 "aliases general" +} + +@test "helper: profile edge case, empty configuration" { + run _bash-it-profile-save "test" + assert_line -n 3 "It seems like no configuration was enabled.." + assert_line -n 4 "Make sure to double check that this is the wanted behavior." + + run _enable-alias "general" + run _enable-plugin "base" + run _enable-plugin "alias-completion" + run _enable-completion "bash-it" + run _enable-completion "system" + + run _bash-it-profile-load "test" + assert_link_not_exist "$BASH_IT/enabled/150---general.aliases.bash" + assert_link_not_exist "$BASH_IT/enabled/250---base.plugin.bash" + assert_link_not_exist "$BASH_IT/enabled/365---alias-completion.plugin.bash" + assert_link_not_exist "$BASH_IT/enabled/350---bash-it.completion.bash" + assert_link_not_exist "$BASH_IT/enabled/350---system.completion.bash" +} + +@test "helper: profile save and load" { + run _enable-alias "general" + run _enable-plugin "base" + run _enable-plugin "alias-completion" + run _enable-completion "bash-it" + run _enable-completion "system" + + run _bash-it-profile-save "test" + assert_success + + run _disable-alias "general" + assert_link_not_exist "$BASH_IT/enabled/150---general.aliases.bash" + run _bash-it-profile-load "test" + assert_link_exist "$BASH_IT/enabled/150---general.aliases.bash" +} + +@test "helper: profile load corrupted profile file: bad component" { + run _bash-it-profile-load "test-bad-component" + assert_line -n 1 -p "Bad line(#12) in profile, aborting load..." +} + +@test "helper: profile load corrupted profile file: bad subdirectory" { + run _bash-it-profile-load "test-bad-type" + assert_line -n 1 -p "Bad line(#5) in profile, aborting load..." +} + +@test "helper: profile rm sanity" { + run _bash-it-profile-save "test" + assert_file_exist "$BASH_IT/profiles/test.bash_it" + run _bash-it-profile-rm "test" + assert_line -n 0 "Removed profile \"test\" successfully!" + assert_file_not_exist "$BASH_IT/profiles/test.bash_it" +} + +@test "helper: profile rm no params" { + run _bash-it-profile-rm "" + assert_line -n 0 -p "Please specify profile name to remove..." +} + +@test "helper: profile load no params" { + run _bash-it-profile-load "" + assert_line -n 0 -p "Please specify profile name to load, not changing configuration..." +} + +@test "helper: profile rm default" { + run _bash-it-profile-rm "default" + assert_line -n 0 -p "Can not remove the default profile..." + assert_file_exist "$BASH_IT/profiles/default.bash_it" +} + +@test "helper: profile rm bad profile name" { + run _bash-it-profile-rm "notexisting" + assert_line -n 0 -p "Could not find profile \"notexisting\"..." +} + +@test "helper: profile list sanity" { + run _bash-it-profile-list + assert_line -n 0 "Available profiles:" + assert_line -n 1 "default" +} + +@test "helper: profile list more profiles" { + run _bash-it-profile-save "cactus" + run _bash-it-profile-save "another" + run _bash-it-profile-save "brother" + run _bash-it-profile-list + assert_line -n 0 "Available profiles:" + assert_line -n 4 "default" + assert_line -n 3 "cactus" + assert_line -n 1 "another" + assert_line -n 2 "brother" +} + @test "helpers: migrate plugins and completions that share the same name" { ln -s $BASH_IT/completion/available/dirs.completion.bash $BASH_IT/completion/enabled/350---dirs.completion.bash assert_link_exist "$BASH_IT/completion/enabled/350---dirs.completion.bash"