diff --git a/.travis.yml b/.travis.yml index d038f60..a16628d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,8 @@ env: - TEST_SHELLS=bash - TEST_SHELLS=zsh +sudo: required + install: - ./test/support/travisci_deps.sh diff --git a/lib/design.sh b/lib/design.sh index 125175e..cd48cdb 100644 --- a/lib/design.sh +++ b/lib/design.sh @@ -34,7 +34,7 @@ design() { local project=`basename $(pwd)` local all_project_dirs="$design_base_dirs $design_av_dirs" # Ensure design dir contains all subdirectories - IFS=$' \t\n' + local IFS=$' \t\n' # Create root design dirs for dir in $design_ext_dirs; do mkdir -p "$root_design_dir/$dir"; done # Create project design dirs @@ -102,6 +102,5 @@ design() { printf "Invalid command.\n\n" design fi - unset IFS } diff --git a/lib/git/aliases.sh b/lib/git/aliases.sh index 91e3c9d..b8184c3 100644 --- a/lib/git/aliases.sh +++ b/lib/git/aliases.sh @@ -17,7 +17,7 @@ unalias git > /dev/null 2>&1 unset -f git > /dev/null 2>&1 # Use the full path to git to avoid infinite loop with git function -export _git_cmd="$(\which git)" +export _git_cmd="$(bin_path git)" # Wrap git with the 'hub' github wrapper, if installed (https://github.com/defunkt/hub) if type hub > /dev/null 2>&1; then export _git_cmd="hub"; fi @@ -71,7 +71,7 @@ __git_alias () { alias_str="$1"; cmd_prefix="$2"; cmd="$3"; if [ $# -gt 2 ]; then shift 3 2>/dev/null - cmd_args=$@ + cmd_args=("$@") fi alias $alias_str="$cmd_prefix $cmd${cmd_args:+ }${cmd_args[*]}" diff --git a/lib/git/branch_shortcuts.sh b/lib/git/branch_shortcuts.sh index 61012e2..27b0ce8 100644 --- a/lib/git/branch_shortcuts.sh +++ b/lib/git/branch_shortcuts.sh @@ -22,7 +22,7 @@ function _scmb_git_branch_shortcuts { # Use ruby to inject numbers into ls output ruby -e "$( cat < 9 && i < 9 ? " " : " ") @@ -32,14 +32,12 @@ EOF )" # Set numbered file shortcut in variable - local e=1 - IFS=$'\n' + local e=1 IFS=$'\n' for branch in $($_git_cmd branch "$@" | sed "s/^[* ]\{2\}//"); do export $git_env_char$e="$branch" if [ "${scmbDebug:-}" = "true" ]; then echo "Set \$$git_env_char$e => $file"; fi let e++ done - unset IFS } __git_alias "$git_branch_alias" "_scmb_git_branch_shortcuts" "" diff --git a/lib/git/fallback/status_shortcuts_shell.sh b/lib/git/fallback/status_shortcuts_shell.sh index 9368588..406da56 100755 --- a/lib/git/fallback/status_shortcuts_shell.sh +++ b/lib/git/fallback/status_shortcuts_shell.sh @@ -16,7 +16,7 @@ # -------------------------------------------------------------------- git_status_shortcuts() { zsh_compat # Ensure shwordsplit is on for zsh - IFS=$'\n' + local IFS=$'\n' local git_status="$(git status --porcelain 2> /dev/null)" local i @@ -95,7 +95,7 @@ git_status_shortcuts() { fi done - IFS=" " + local IFS=" " grp_num=1 for heading in 'Changes to be committed' 'Unmerged paths' 'Changes not staged for commit' 'Untracked files'; do # If no group specified as param, or specified group is current group @@ -114,7 +114,6 @@ git_status_shortcuts() { # so just use plain 'git status' git status fi - unset IFS zsh_reset # Reset zsh environment to default } # Template function for 'git_status_shortcuts'. diff --git a/lib/git/helpers.sh b/lib/git/helpers.sh index f2d4107..785bd60 100644 --- a/lib/git/helpers.sh +++ b/lib/git/helpers.sh @@ -13,4 +13,11 @@ function fail_if_not_git_repo() { return 1 fi return 0 -} \ No newline at end of file +} + +bin_path() { + if [[ -n ${ZSH_VERSION:-} ]]; + then builtin whence -cp "$1" 2> /dev/null + else builtin type -P "$1" + fi +} diff --git a/lib/git/repo_index.sh b/lib/git/repo_index.sh index 3d2e9c7..9edcd24 100644 --- a/lib/git/repo_index.sh +++ b/lib/git/repo_index.sh @@ -51,7 +51,7 @@ function git_index() { - IFS=$'\n' + local IFS=$'\n' if [ -z "$1" ]; then # Just change to $GIT_REPO_DIR if no params given. "cd" $GIT_REPO_DIR @@ -103,7 +103,7 @@ function git_index() { # -------------------- # Go to our base path if [ -n "$base_path" ]; then - IFS=$' \t\n' + local IFS=$' \t\n' # evaluate ~ if necessary if [[ "$base_path" == "~"* ]]; then base_path=$(eval echo ${base_path%%/*})/${base_path#*/} @@ -116,7 +116,6 @@ function git_index() { fi fi fi - unset IFS } _git_index_dirs_without_home() { @@ -126,12 +125,11 @@ _git_index_dirs_without_home() { # Recursively searches for git repos in $GIT_REPO_DIR function _find_git_repos() { # Find all unarchived projects - IFS=$'\n' + local IFS=$'\n' for repo in $(find -L "$GIT_REPO_DIR" -maxdepth 5 -name ".git" -type d \! -wholename '*/archive/*'); do echo ${repo%/.git} # Return project folder, with trailing ':' _find_git_submodules $repo # Detect any submodules done - unset IFS } # List all submodules for a git repo, if any. @@ -146,11 +144,10 @@ function _find_git_submodules() { function _rebuild_git_index() { if [ "$1" != "--silent" ]; then echo -e "== Scanning $GIT_REPO_DIR for git repos & submodules..."; fi # Get repos from src dir and custom dirs, then sort by basename - IFS=$'\n' + local IFS=$'\n' for repo in $(echo -e "$(_find_git_repos)\n$(echo $GIT_REPOS | sed "s/:/\\\\n/g")"); do echo $(basename $repo | sed "s/ /_/g"):$repo done | sort -t ":" -k1,1 | cut -d ":" -f2- >| "$GIT_REPO_DIR/.git_index" - unset IFS if [ "$1" != "--silent" ]; then echo -e "===== Indexed $_bld_col$(_git_index_count)$_txt_col repos in $GIT_REPO_DIR/.git_index" @@ -205,39 +202,39 @@ _git_index_update_all_branches() { return fi - local remotes merges branches + # zsh 5.0.2 requires local separate to assignment for arrays + local remote merge remotes merges branches # Get branch configuration from .git/config - IFS=$'\n' + local IFS=$'\n' for branch in $($GIT_BINARY branch 2> /dev/null | sed -e 's/.\{2\}\(.*\)/\1/'); do # Skip '(no branch)' if [[ "$branch" = "(no branch)" ]]; then continue; fi - local remote=$(git config --get branch.$branch.remote) - local merge=$(git config --get branch.$branch.merge) + remote=$(git config --get "branch.$branch.remote") + merge=$(git config --get "branch.$branch.merge") # Ignore branch if remote and merge is not configured if [[ -n "$remote" ]] && [[ -n "$merge" ]]; then - branches=(${branches[@]} "$branch") - remotes=(${remotes[@]} "$remote") + branches=("${branches[@]}" "$branch") + remotes=("${remotes[@]}" "$remote") # Get branch from merge ref (refs/heads/master => master) - merges=(${merges[@]} "$(basename $merge)") + merges=("${merges[@]}" "$(basename "$merge")") else echo "=== Skipping $branch: remote and merge refs are not configured." fi done - unset IFS # Update all remotes if there are any branches to update if [ -n "${branches[*]}" ]; then git fetch --all 2> /dev/null; fi local index=0 # Iterate over branches, and update those that can be fast-forwarded - for branch in ${branches[@]}; do + for branch in "${branches[@]}"; do branch_rev="$(git rev-parse $branch)" # Local branch can be fast-forwarded if revision is ancestor of remote revision, and not the same. # (see http://stackoverflow.com/a/2934062/304706) - if [[ "$branch_rev" != "$(git rev-parse ${remotes[$index]}/${merges[$index]})" ]] && \ - [[ "$(git merge-base $branch_rev ${remotes[$index]}/${merges[$index]})" = "$branch_rev" ]]; then + if [[ "$branch_rev" != "$(git rev-parse "${remotes[$index]}/${merges[$index]}")" ]] && \ + [[ "$(git merge-base "$branch_rev" "${remotes[$index]}/${merges[$index]}")" = "$branch_rev" ]]; then echo "=== Updating $branch branch in $base_path from ${remotes[$index]}/${merges[$index]}..." # Checkout branch if we aren't already on it. if [[ "$branch" != "$(parse_git_branch)" ]]; then git checkout $branch; fi @@ -268,11 +265,11 @@ function _git_index_batch_cmd() { cwd="$PWD" if [ -n "$1" ]; then echo -e "== Running command for $_bld_col$(_git_index_count)$_txt_col repos...\n" - unset IFS + local IFS=$'\n' local base_path for base_path in $(sed -e "s/--.*//" "$GIT_REPO_DIR/.git_index" | \grep . | sort); do builtin cd "$base_path" - $@ + "$@" done else echo "Please give a command to run for all repos. (It may be useful to write your command as a function or script.)" @@ -285,8 +282,7 @@ if [ $shell = 'bash' ]; then # Bash tab completion function for git_index() function _git_index_tab_completion() { _check_git_index - local curw - IFS=$'\n' + local curw IFS=$'\n' COMPREPLY=() curw=${COMP_WORDS[COMP_CWORD]} @@ -313,10 +309,9 @@ if [ $shell = 'bash' ]; then else COMPREPLY=($(compgen -W '$(sed -e "s:.*/::" -e "s:$:/:" "$GIT_REPO_DIR/.git_index" | sort)' -- $curw)) fi - unset IFS return 0 } -else +else # Zsh tab completion function for git_index() function _git_index_tab_completion() { typeset -A opt_args local state state_descr context line diff --git a/lib/git/shell_shortcuts.sh b/lib/git/shell_shortcuts.sh index 9b5423e..49d9b81 100644 --- a/lib/git/shell_shortcuts.sh +++ b/lib/git/shell_shortcuts.sh @@ -23,19 +23,19 @@ if [ "$shell_command_wrapping_enabled" = "true" ] || [ "$bash_command_wrapping_e # Define 'whence' for bash, to get the value of an alias type whence > /dev/null 2>&1 || function whence() { LC_MESSAGES="C" type "$@" | sed -$SED_REGEX_ARG -e "s/.*is aliased to \`//" -e "s/'$//"; } local cmd='' - for cmd in $(echo $scmb_wrapped_shell_commands); do + for cmd in $scmb_wrapped_shell_commands; do if [ "${scmbDebug:-}" = "true" ]; then echo "SCMB: Wrapping $cmd..."; fi # Special check for 'cd', to make sure SCM Breeze is loaded after RVM if [ "$cmd" = 'cd' ]; then if [ -e "$HOME/.rvm" ] && ! type rvm > /dev/null 2>&1; then - echo -e "\033[0;31mSCM Breeze must be loaded \033[1;31mafter\033[0;31m RVM, otherwise there will be a conflict when RVM wraps the 'cd' command.\033[0m" - echo -e "\033[0;31mPlease move the line that loads SCM Breeze to the bottom of your ~/.bashrc\033[0m" + echo -e "\\033[0;31mSCM Breeze must be loaded \\033[1;31mafter\\033[0;31m RVM, otherwise there will be a conflict when RVM wraps the 'cd' command.\\033[0m" + echo -e "\\033[0;31mPlease move the line that loads SCM Breeze to the bottom of your ~/.bashrc\\033[0m" continue fi fi - case "$(LC_MESSAGES="C" type $cmd 2>&1)" in + case "$(LC_MESSAGES="C" type "$cmd" 2>&1)" in # Don't do anything if command already aliased, or not found. *'exec_scmb_expand_args'*) @@ -49,10 +49,10 @@ if [ "$shell_command_wrapping_enabled" = "true" ] || [ "$bash_command_wrapping_e # Store original alias local original_alias="$(whence $cmd)" # Remove alias, so that we can find binary - unalias $cmd + unalias "$cmd" # Detect original $cmd type, and escape - case "$(LC_MESSAGES="C" type $cmd 2>&1)" in + case "$(LC_MESSAGES="C" type "$cmd" 2>&1)" in # Escape shell builtins with 'builtin' *'is a shell builtin'*) local escaped_cmd="builtin $cmd";; # Get full path for files with 'find_binary' function @@ -67,9 +67,9 @@ if [ "$shell_command_wrapping_enabled" = "true" ] || [ "$bash_command_wrapping_e *'is a'*'function'*) if [ "${scmbDebug:-}" = "true" ]; then echo "SCMB: $cmd is a function"; fi # Copy old function into new name - eval "$(declare -f $cmd | sed -$SED_REGEX_ARG "s/^$cmd \(\)/__original_$cmd ()/")" + eval "$(declare -f "$cmd" | sed -"$SED_REGEX_ARG" "s/^$cmd \\(\\)/__original_$cmd ()/")" # Remove function - unset -f $cmd + unset -f "$cmd" # Create function that wraps old function eval "${cmd}(){ exec_scmb_expand_args __original_${cmd} \"\$@\"; }";; @@ -100,64 +100,85 @@ if ! ls --color=auto > /dev/null 2>&1; then fi # Test if readlink supports -f option, otherwise use perl (a bit slower) -if ! readlink -f > /dev/null 2>&1; then - _abs_path_command='perl -e "use Cwd "abs_path"; print abs_path(shift)"' +if ! readlink -f / > /dev/null 2>&1; then + _abs_path_command=(perl -e 'use Cwd abs_path; print abs_path(shift)') else - _abs_path_command="readlink -f" + _abs_path_command=(readlink -f) fi # Function wrapper around 'll' # Adds numbered shortcuts to output of ls -l, just like 'git status' -if [ "$shell_ls_aliases_enabled" = "true" ] && which ruby > /dev/null 2>&1; then +if [ "$shell_ls_aliases_enabled" = "true" ] && builtin command -v ruby > /dev/null 2>&1; then unalias ll > /dev/null 2>&1; unset -f ll > /dev/null 2>&1 function ls_with_file_shortcuts { local ll_output + local ll_command # Ensure sort ordering of the two invocations is the same if [ "$_ls_bsd" != "BSD" ]; then - ll_output="$(\ls -lhv --group-directories-first --color "$@")" + ll_command=(\ls -hv --group-directories-first) + ll_output="$("${ll_command[@]}" -l --color "$@")" else - ll_output="$(CLICOLOR_FORCE=1 \ls -l -G "$@")" + ll_command=(\ls) + ll_output="$(CLICOLOR_FORCE=1 "${ll_command[@]}" -lG "$@")" fi if [[ $shell == "zsh" ]]; then # Ensure sh_word_split is on - if setopt | grep -q shwordsplit; then SHWORDSPLIT_ON=true; fi + [[ -o shwordsplit ]] && SHWORDSPLIT_ON=true setopt shwordsplit fi - # Parse path from args - IFS=$'\n' - for arg in $@; do - if [ -d "$arg" ]; then local rel_path="${arg%/}"; fi + # Get the directory that `ls` is being run relative to. + # Only allow one directory to avoid incorrect $e# variables when listing + # multiple directories (issue #274) + local IFS=$'\n' + local rel_path + for arg in "$@"; do + if [[ -e $arg ]]; then # Path rather than option to ls + if [[ -z $rel_path ]]; then # We are seeing our first pathname + if [[ -d $arg ]]; then # It's a directory + rel_path=$arg + else # It's a file, expand the current directory + rel_path=. + fi + elif [[ -d $arg || ( -f $arg && $rel_path != . ) ]]; then + if [[ -f $arg ]]; then arg=$PWD; fi # Get directory for current argument + # We've already seen a different directory. Quit to avoid damage (issue #274) + printf 'scm_breeze: Cannot list relative to both directories:\n %s\n %s\n' "$arg" "$rel_path" >&2 + printf 'Currently only listing a single directory is supported. See issue #274.\n' >&2 + return 1 + fi + fi done - unset IFS + rel_path=$("${_abs_path_command[@]}" ${rel_path:-$PWD}) # Replace user/group with user symbol, if defined at ~/.user_sym # Before : -rw-rw-r-- 1 ndbroadbent ndbroadbent 1.1K Sep 19 21:39 scm_breeze.sh # After : -rw-rw-r-- 1 𝐍 𝐍 1.1K Sep 19 21:39 scm_breeze.sh - if [ -e $HOME/.user_sym ]; then + if [ -e "$HOME"/.user_sym ]; then # Little bit of ruby golf to rejustify the user/group/size columns after replacement + # TODO(ghthor): Convert this to a cat <&2 echo "$ll_output" return 1 fi @@ -180,23 +201,26 @@ EOF local ll_files='' local file='' + # XXX FIXME XXX + # There is a race condition here: If a file is removed between the above + # and this second call of `ls` then the $e# variables can refer to the + # wrong files. if [ -z $_ls_bsd ]; then - ll_files="$(\ls -v --group-directories-first --color=never "$@")" + ll_files="$(QUOTING_STYLE=literal "${ll_command[@]}" --color=never "$@")" else - ll_files="$(\ls "$@")" + ll_files="$("${ll_command[@]}" "$@")" fi - IFS=$'\n' + local IFS=$'\n' for file in $ll_files; do - if [ -n "$rel_path" ]; then file="$rel_path/$file"; fi - export $git_env_char$e="$(eval $_abs_path_command \"${file//\"/\\\"}\")" - if [ "${scmbDebug:-}" = "true" ]; then echo "Set \$$git_env_char$e => $file"; fi + file=$rel_path/$file + export $git_env_char$e=$("${_abs_path_command[@]}" "$file") + if [[ ${scmbDebug:-} = true ]]; then echo "Set \$$git_env_char$e => $file"; fi let e++ done - unset IFS # Turn off shwordsplit unless it was on previously - if [[ $shell == "zsh" ]] && [ -z "$SHWORDSPLIT_ON" ]; then unsetopt shwordsplit; fi + if [[ $shell == "zsh" && -z $SHWORDSPLIT_ON ]]; then unsetopt shwordsplit; fi } # Setup aliases diff --git a/lib/git/status_shortcuts.sh b/lib/git/status_shortcuts.sh index bd585c0..54b3191 100644 --- a/lib/git/status_shortcuts.sh +++ b/lib/git/status_shortcuts.sh @@ -21,7 +21,7 @@ git_status_shortcuts() { zsh_compat # Ensure shwordsplit is on for zsh git_clear_vars # Run ruby script, store output - local cmd_output="$(/usr/bin/env ruby "$scmbDir/lib/git/status_shortcuts.rb" $@)" + local cmd_output="$(/usr/bin/env ruby "$scmbDir/lib/git/status_shortcuts.rb" "$@")" # Print debug information if $scmbDebug = "true" if [ "${scmbDebug:-}" = "true" ]; then printf "status_shortcuts.rb output => \n$cmd_output\n------------------------\n" @@ -36,14 +36,13 @@ git_status_shortcuts() { files="$(echo "$cmd_output" | \grep '@@filelist@@::' | sed 's%@@filelist@@::%%g')" if [ "${scmbDebug:-}" = "true" ]; then echo "filelist => $files"; fi # Export numbered env variables for each file - IFS="|" + local IFS="|" local e=1 for file in $files; do export $git_env_char$e="$file" if [ "${scmbDebug:-}" = "true" ]; then echo "Set \$$git_env_char$e => $file"; fi let e++ done - unset IFS if [ "${scmbDebug:-}" = "true" ]; then echo "------------------------"; fi # Print status @@ -80,10 +79,11 @@ git_add_shortcuts() { git_silent_add_shortcuts() { if [ -n "$1" ]; then # Expand args and process resulting set of files. - IFS=$'\t' - for file in $(scmb_expand_args "$@"); do + local args + eval args="$(scmb_expand_args "$@")" # populate $args array + for file in "${args[@]}"; do # Use 'git rm' if file doesn't exist and 'ga_auto_remove' is enabled. - if [[ $ga_auto_remove == "yes" ]] && ! [ -e "$file" ]; then + if [[ $ga_auto_remove = yes && ! -e $file ]]; then echo -n "# " git rm "$file" else @@ -91,7 +91,6 @@ git_silent_add_shortcuts() { echo -e "# Added '$file'" fi done - unset IFS echo "#" fi } @@ -100,18 +99,18 @@ git_silent_add_shortcuts() { # and exports numbered environment variables for each file. git_show_affected_files(){ fail_if_not_git_repo || return 1 - f=0 # File count + local f=0 # File count # Show colored revision and commit message - echo -n "# "; git show --oneline --name-only $@ | head -n1; echo "# " - for file in $(git show --pretty="format:" --name-only $@ | \grep -v '^$'); do + echo -n "# "; git show --oneline --name-only "$@" | head -n1; echo "# " + for file in $(git show --pretty="format:" --name-only "$@" | \grep -v '^$'); do let f++ export $git_env_char$f=$file # Export numbered variable. echo -e "# \033[2;37m[\033[0m$f\033[2;37m]\033[0m $file" done; echo "# " } - # Allows expansion of numbered shortcuts, ranges of shortcuts, or standard paths. +# Return a string which can be `eval`ed like: eval args="$(scmb_expand_args "$@")" # Numbered shortcut variables are produced by various commands, such as: # * git_status_shortcuts() - git status implementation # * git_show_affected_files() - shows files affected by a given SHA1, etc. @@ -122,43 +121,52 @@ scmb_expand_args() { shift fi - first=1 - OLDIFS="$IFS"; IFS=" " # We need to split on spaces to loop over expanded range + local args + args=() # initially empty array. zsh 5.0.2 from Ubuntu 14.04 requires this to be separated for arg in "$@"; do if [[ "$arg" =~ ^[0-9]{0,4}$ ]] ; then # Substitute $e{*} variables for any integers - if [ "$first" -eq 1 ]; then first=0; else printf '\t'; fi if [ -e "$arg" ]; then # Don't expand files or directories with numeric names - printf '%s' "$arg" + args+=("$arg") else - _print_path "$relative" "$git_env_char$arg" + args+=("$(_print_path "$relative" "$git_env_char$arg")") fi elif [[ "$arg" =~ ^[0-9]+-[0-9]+$ ]]; then # Expand ranges into $e{*} variables - for i in $(eval echo {${arg/-/..}}); do - if [ "$first" -eq 1 ]; then first=0; else printf '\t'; fi - _print_path "$relative" "$git_env_char$i" + args+=("$(_print_path "$relative" "$git_env_char$i")") done else # Otherwise, treat $arg as a normal string. - if [ "$first" -eq 1 ]; then first=0; else printf '\t'; fi - printf '%s' "$arg" + args+=("$arg") fi done - IFS="$OLDIFS" + + # "declare -p" with zsh 5.0.2 on Ubuntu 14.04 creates a string that it cannot process: + # typeset -a args args=(one three six) + # There should be a ; between the two "args" tokens + # "declare -p" with bash 4.3.11(1) on Ubuntu 14.04 creates a string like: + # declare -a a='([0]="a" [1]="b c" [2]="d")' + # The RHS of this string is incompatible with zsh 5.0.2 and "eval args=" + + # Generate a quoted array string to assign to "eval args=" + echo "( $(token_quote "${args[@]}") )" } +# Expand a variable (named by $2) into a (possibly relative) pathname _print_path() { - if [ "$1" = 1 ]; then - eval printf '%s' "\"\$$2\"" | sed -e "s%$(pwd)/%%" | awk '{printf("%s", $0)}' - else - eval printf '%s' "\"\$$2\"" + local pathname + pathname=$(eval printf '%s' "\"\${$2}\"") + if [ "$1" = 1 ]; then # print relative + pathname=${pathname#$PWD/} # Remove $PWD from beginning of the path fi + printf '%s' "$pathname" } # Execute a command with expanded args, e.g. Delete files 6 to 12: $ ge rm 6-12 # Fails if command is a number or range (probably not worth fixing) exec_scmb_expand_args() { - eval "$(scmb_expand_args "$@" | sed -e "s/\([][|;()<>^ \"'&]\)/"'\\\1/g')" + local args + eval "args=$(scmb_expand_args "$@")" # populate $args array + _safe_eval "${args[@]}" } # Clear numbered env variables @@ -180,13 +188,13 @@ git_clear_vars() { _git_resolve_merge_conflict() { if [ -n "$2" ]; then # Expand args and process resulting set of files. - IFS=$'\t' - for file in $(scmb_expand_args "${@:2}"); do + local args + eval "args=$(scmb_expand_args "$@")" # populate $args array + for file in "${args[@]:2}"; do git checkout "--$1""s" "$file" # "--$1""s" is expanded to --ours or --theirs git add "$file" echo -e "# Added $1 version of '$file'" done - unset IFS echo -e "# -- If you have finished resolving conflicts, commit the resolutions with 'git commit'" fi } diff --git a/lib/git/tools.sh b/lib/git/tools.sh index 0c62e72..c158a77 100644 --- a/lib/git/tools.sh +++ b/lib/git/tools.sh @@ -23,8 +23,9 @@ git_remove_history() { return fi # Remove all paths passed as arguments from the history of the repo - files=$@ - $_git_cmd filter-branch --index-filter "$_git_cmd rm -rf --cached --ignore-unmatch $files" HEAD + local files + files=("$@") + $_git_cmd filter-branch --index-filter "$_git_cmd rm -rf --cached --ignore-unmatch ${files[*]}" HEAD # Remove the temporary history git-filter-branch otherwise leaves behind for a long time rm -rf .git/refs/original/ && $_git_cmd reflog expire --all && $_git_cmd gc --aggressive --prune } @@ -142,4 +143,4 @@ git_branch_delete_all() { commit_docs() { git commit -m "Update README / Documentation [ci skip]" -} \ No newline at end of file +} diff --git a/lib/scm_breeze.sh b/lib/scm_breeze.sh index 2c692bb..2744b00 100644 --- a/lib/scm_breeze.sh +++ b/lib/scm_breeze.sh @@ -18,6 +18,48 @@ _alias() { fi } +# Quote the contents of "$@" +function token_quote { + # Older versions of {ba,z}sh don't support the built-in quoting, so fall back to printf %q + local quoted + quoted=() # Assign separately for zsh 5.0.2 of Ubuntu 14.04 + for token; do + quoted+=( "$(printf '%q' "$token")" ) + done + printf '%s\n' "${quoted[*]}" + + # Keep this code for use when minimum versions of {ba,z}sh can be increased. + # See https://github.com/scmbreeze/scm_breeze/issues/260 + # + # if [[ $shell = bash ]]; then + # # ${parameter@operator} where parameter is ${@} and operator is 'Q' + # # https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html + # eval "${@@Q}" + # else # zsh + # # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags + # eval "${(q-)@}" + # fi +} + +# Quote "$@" before `eval` to prevent arbitrary code execution. +# Eg, the following will run `date`: +# evil() { eval "$@"; }; evil "echo" "foo;date" +function _safe_eval() { + eval $(token_quote "$@") + + # Keep this code for use when minimum versions of {ba,z}sh can be increased. + # See https://github.com/scmbreeze/scm_breeze/issues/260 + # + # if [[ $shell = bash ]]; then + # # ${parameter@operator} where parameter is ${@} and operator is 'Q' + # # https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html + # eval "${@@Q}" + # else # zsh + # # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags + # eval "${(q-)@}" + # fi +} + find_binary(){ if [ $shell = "zsh" ]; then builtin type -p "$1" | sed "s/$1 is //" | head -1 diff --git a/run_tests.sh b/run_tests.sh index e25c5eb..c0a3149 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -8,8 +8,9 @@ failed=false if [ -z "$TEST_SHELLS" ]; then TEST_SHELLS="bash zsh" fi - echo "== Will run all tests with following shells: ${TEST_SHELLS}" + +cd -P -- "${0%/*}" # Change to directory this script lives in for test in $(find test/lib -name *_test.sh); do for shell in $TEST_SHELLS; do echo "== Running tests with [$shell]: $test" diff --git a/test/lib/git/repo_index_test.sh b/test/lib/git/repo_index_test.sh index c866ec4..eb98334 100755 --- a/test/lib/git/repo_index_test.sh +++ b/test/lib/git/repo_index_test.sh @@ -68,13 +68,12 @@ EOF done # Setup some custom repos outside the main repo dir - IFS=":" + local IFS=":" for dir in $GIT_REPOS; do mkdir -p $dir cd $dir git init done - unset IFS verboseGitCommands @@ -83,9 +82,8 @@ EOF oneTimeTearDown() { rm -rf "${GIT_REPO_DIR}" - IFS=":" + local IFS=":" for dir in $GIT_REPOS; do rm -rf $dir; done - unset IFS } ensureIndex() { diff --git a/test/lib/git/shell_shortcuts_test.sh b/test/lib/git/shell_shortcuts_test.sh index f699085..6e470a7 100755 --- a/test/lib/git/shell_shortcuts_test.sh +++ b/test/lib/git/shell_shortcuts_test.sh @@ -21,13 +21,6 @@ fi source "$scmbDir/test/support/test_helper.sh" source "$scmbDir/lib/scm_breeze.sh" -bin_path() { - if [ -n "${ZSH_VERSION:-}" ]; - then where "$@" | tail -1 - else which "$@" - fi -} - # Setup #----------------------------------------------------------------------------- oneTimeSetUp() { @@ -38,7 +31,7 @@ oneTimeSetUp() { alias rvm="test" # Ensure tests run if RVM isn't loaded but $HOME/.rvm is present # Test functions - function ln() { ln $@; } + function ln() { ln "$@"; } # Before aliasing, get original locations so we can compare them in the test unalias mv rm sed cat 2>/dev/null @@ -69,6 +62,15 @@ assertAliasEquals(){ } +#----------------------------------------------------------------------------- +# Setup and tear down +#----------------------------------------------------------------------------- + +setUp() { + unset QUOTING_STYLE # Use default quoting style for ls +} + + #----------------------------------------------------------------------------- # Unit tests #----------------------------------------------------------------------------- @@ -93,18 +95,18 @@ test_ls_with_file_shortcuts() { # full physical path to be absolutely certain when doing comparisons later, # because thats how the Ruby status_shortcuts.rb script is going to obtain # them. - cd $TEST_DIR + cd "$TEST_DIR" TEST_DIR=$(pwd -P) touch 'test file' 'test_file' mkdir -p "a [b]" 'a "b"' "a 'b'" - touch "a \"b\"/c" + touch 'a "b"/c' # Run command in shell, load output from temp file into variable # (This is needed so that env variables are exported in the current shell) temp_file=$(mktemp -t scm_breeze.XXXXXXXXXX) - ls_with_file_shortcuts > $temp_file - ls_output=$(<$temp_file strip_colors) + ls_with_file_shortcuts > "$temp_file" + ls_output=$(<"$temp_file" strip_colors) # Compare as fixed strings (F), instead of normal grep behavior assertIncludes "$ls_output" '[1] a "b"' F @@ -125,10 +127,24 @@ test_ls_with_file_shortcuts() { ls_output=$(<$temp_file strip_colors) assertIncludes "$ls_output" '[1] c' F # Test that env variable is set correctly - assertEquals "$TEST_DIR/a \"b\"/c" "$e1" + assertEquals "$TEST_DIR/"'a "b"/c' "$e1" # Test arg with no quotes ls_output=$(ls_with_file_shortcuts a\ \"b\" | strip_colors) assertIncludes "$ls_output" '[1] c' F + + # Listing two directories fails (see issue #275) + mkdir 1 2 + touch 1/file + assertFalse 'Only one directory supported' 'ls_with_file_shortcuts 1 2' + assertFalse 'Fails on ' 'ls_with_file_shortcuts 1 test_file' + assertFalse 'Fails on ' 'ls_with_file_shortcuts test_file 1' + assertFalse 'Fails on /' 'ls_with_file_shortcuts 1 1/file' + + # Files under the root directory + assertTrue 'Shortcuts under /' 'ls_with_file_shortcuts / > /dev/null && [[ $e1 =~ ^/[^/]+$ ]]' + + cd - + rm -r "$TEST_DIR" "$temp_file" } # load and run shUnit2 diff --git a/test/lib/git/status_shortcuts_test.sh b/test/lib/git/status_shortcuts_test.sh index 638858a..eb3acae 100755 --- a/test/lib/git/status_shortcuts_test.sh +++ b/test/lib/git/status_shortcuts_test.sh @@ -49,21 +49,72 @@ setupTestRepo() { #----------------------------------------------------------------------------- test_scmb_expand_args() { - local e1="one"; local e2="two"; local e3="three"; local e4="four"; local e5="five"; local e6="six"; local e7="seven" + local e1="one" e2="two" e3="three" e4="four" e5="five" e6="six" e7='$dollar' e8='two words' local error="Args not expanded correctly" - assertEquals "$error" "$(printf 'one\tthree\tseven')" "$(scmb_expand_args 1 3 7)" - assertEquals "$error" "$(printf 'one\ttwo\tthree\tsix')" "$(scmb_expand_args 1-3 6)" - assertEquals "$error" "$(printf 'seven\ttwo\tthree\tfour\tfive\tone')" "$(scmb_expand_args seven 2-5 1)" + assertEquals "$error" 'one three six' \ + "$(eval args="$(scmb_expand_args 1 3 6)"; token_quote "${args[@]}")" + assertEquals "$error" 'one two three five' \ + "$(eval args="$(scmb_expand_args 1-3 5)"; token_quote "${args[@]}")" + assertEquals "$error" '\$dollar two three four one' \ + "$(eval args="$(scmb_expand_args 7 2-4 1)"; token_quote "${args[@]}")" # Test that any args with spaces remain quoted - assertEquals "$error" "$(printf -- '-m\tTest Commit Message\tone')" "$(scmb_expand_args -m "Test Commit Message" 1)" - assertEquals "$error" "$(printf -- '-ma\tTest Commit Message\tUnquoted')"\ - "$(scmb_expand_args -ma "Test Commit Message" "Unquoted")" + assertEquals "$error" '-m Test\ Commit\ Message one' \ + "$(eval args="$(scmb_expand_args -m "Test Commit Message" 1)"; token_quote "${args[@]}")" + assertEquals "$error" '-ma Test\ Commit\ Message Unquoted'\ + "$(eval args="$(scmb_expand_args -ma "Test Commit Message" "Unquoted")"; token_quote "${args[@]}")" + assertEquals "$error" '\$dollar one two\ words' \ + "$(eval args="$(scmb_expand_args 7 1-1 8)"; token_quote "${args[@]}")" + + # Keep this code for use when minimum versions of {ba,z}sh can be increased. + # See token_quote() source and https://github.com/scmbreeze/scm_breeze/issues/260 + # local e1="one" e2="two" e3="three" e4="four" e5="five" e6="six" e7='$dollar' e8='two words' + # local error="Args not expanded correctly" + # assertEquals "$error" "'one' 'three' 'six'" \ + # "$(eval a=$(scmb_expand_args 1 3 6); token_quote "${a[@]}")" + # assertEquals "$error" "'one' 'two' 'three' 'five'" \ + # "$(eval a=$(scmb_expand_args 1-3 5); token_quote "${a[@]}")" + # assertEquals "$error" "'\$dollar' 'two' 'three' 'four' 'one'" \ + # "$(eval a=$(scmb_expand_args 7 2-4 1); token_quote "${a[@]}")" + # + # # Test that any args with spaces remain quoted + # assertEquals "$error" "'-m' 'Test Commit Message' 'one'" \ + # "$(eval a=$(scmb_expand_args -m "Test Commit Message" 1); token_quote "${a[@]}")" + # assertEquals "$error" "'-ma' 'Test Commit Message' 'Unquoted'"\ + # "$(eval a=$(scmb_expand_args -ma "Test Commit Message" "Unquoted"); token_quote "${a[@]}")" + # assertEquals "$error" "'\$dollar' 'one' 'two words'" \ + # "$(eval a=$(scmb_expand_args 7 1-1 8); token_quote "${a[@]}")" +} + +test_exec_scmb_expand_args() { + local e1="one" e2="a b c" e3='$dollar' e4="single'quote" e5='double"quote' e6='a(){:;};a&' + assertEquals "literals with spaces not preserved" 'foo bar\ baz' \ + "$(eval args="$(scmb_expand_args foo 'bar baz')"; token_quote "${args[@]}")" + assertEquals "variables with spaces not preserved" 'one a\ b\ c' \ + "$(eval args="$(scmb_expand_args 1-2)"; token_quote "${args[@]}")" + # Expecting text: '$dollar' "single'quote" 'double"quote' + # Generate quoted expected string with: token_quote "$(cat)" then copy/paste, ^D + assertEquals "special characters are preserved" \ + '\$dollar single\'\''quote double\"quote a\(\)\{:\;\}\;a\&' \ + "$(eval args="$(scmb_expand_args 3-6)"; token_quote "${args[@]}")" + + # Keep this code for use when minimum versions of {ba,z}sh can be increased. + # See token_quote() source and https://github.com/scmbreeze/scm_breeze/issues/260 + # local e1="one" e2="a b c" e3='$dollar' e4="single'quote" e5='double"quote' e6='a(){:;};a&' + # assertEquals "literals with spaces not preserved" "'foo' 'bar baz'" \ + # "$(eval a="$(scmb_expand_args foo 'bar baz')"; token_quote "${a[@]}")" + # assertEquals "variables with spaces not preserved" "'one' 'a b c'" \ + # "$(eval a="$(scmb_expand_args 1-2)"; token_quote "${a[@]}")" + # # Expecting text: '$dollar' "single'quote" 'double"quote' + # # Generate quoted expected string with: token_quote "$(cat)" then copy/paste, ^D + # assertEquals "special characters are preserved" \ + # "'\$dollar' 'single'\\''quote' 'double\"quote' 'a(){:;};a&'" \ + # "$(eval a="$(scmb_expand_args 3-6)"; token_quote "${a[@]}")" } test_command_wrapping_escapes_special_characters() { - assertEquals 'should escape | the pipe' "$(exec_scmb_expand_args echo "should escape | the pipe")" - assertEquals 'should escape ; the semicolon' "$(exec_scmb_expand_args echo "should escape ; the semicolon")" + assertEquals 'should escape | the pipe' "$(exec_scmb_expand_args echo should escape '|' the pipe)" + assertEquals 'should escape ; the semicolon' "$(exec_scmb_expand_args echo should escape ';' the semicolon)" } test_git_status_shortcuts() { diff --git a/test/lib/scm_breeze_test.sh b/test/lib/scm_breeze_test.sh new file mode 100755 index 0000000..5a5a8da --- /dev/null +++ b/test/lib/scm_breeze_test.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +export scmbDir="$( cd -P "$( dirname "$0" )" && pwd )/../.." + +# Zsh compatibility +if [ -n "${ZSH_VERSION:-}" ]; then shell="zsh"; SHUNIT_PARENT=$0; setopt shwordsplit; fi + +# Load test helpers +source "$scmbDir/test/support/test_helper.sh" + +# Load functions to test +source "$scmbDir/lib/scm_breeze.sh" + +#----------------------------------------------------------------------------- +# Unit tests +#----------------------------------------------------------------------------- + +test__safe_eval() { + assertEquals "runs eval with simple words" 'one two three' "$(_safe_eval token_quote one two three)" + assertEquals "quotes spaces" 'a b\ c d' "$(_safe_eval token_quote a b\ c d)" + assertEquals "quotes special chars" 'a\ b \$dollar \\slash c\ d' "$(_safe_eval token_quote a\ b '$dollar' '\slash' c\ d)" +} + + +# load and run shUnit2 +source "$scmbDir/test/support/shunit2" diff --git a/test/support/shunit2 b/test/support/shunit2 index 8ca4b4b..df37583 100755 --- a/test/support/shunit2 +++ b/test/support/shunit2 @@ -25,9 +25,9 @@ SHUNIT_ERROR=2 # enable strict mode by default SHUNIT_STRICT=${SHUNIT_STRICT:-${SHUNIT_TRUE}} -_shunit_warn() { echo "shunit2:WARN $@" >&2; } -_shunit_error() { echo "shunit2:ERROR $@" >&2; } -_shunit_fatal() { echo "shunit2:FATAL $@" >&2; exit ${SHUNIT_ERROR}; } +_shunit_warn() { echo "shunit2:WARN $*" >&2; } +_shunit_error() { echo "shunit2:ERROR $*" >&2; } +_shunit_fatal() { echo "shunit2:FATAL $*" >&2; exit ${SHUNIT_ERROR}; } # specific shell checks if [ -n "${ZSH_VERSION:-}" ]; then diff --git a/test/support/test_helper.sh b/test/support/test_helper.sh index a021b03..67a6155 100644 --- a/test/support/test_helper.sh +++ b/test/support/test_helper.sh @@ -4,9 +4,9 @@ orig_cwd="$PWD" source "$scmbDir/lib/git/helpers.sh" # Set up demo git user if not configured -if [ -z "$(git config --global user.email)" ]; then - git config --global user.email "testuser@example.com" - git config --global user.name "Test User" +if [ -z "$(git config user.email)" ]; then + git config user.email "testuser@example.com" + git config user.name "Test User" fi # @@ -26,24 +26,32 @@ tab_completions(){ echo "${COMPREPLY[@]}"; } silentGitCommands() { git() { /usr/bin/env git "$@" > /dev/null 2>&1; } } + # Cancel silent git commands verboseGitCommands() { unset -f git } + # Asserts #----------------------------------------------------------------------------- +# Return 0 (shell's true) if "$1" contains string "$2" _includes() { if [ -n "$3" ]; then regex="$3"; else regex=''; fi - if echo "$1" | grep -q$regex "$2"; then echo 0; else echo 1; fi + echo "$1" | grep -q"$regex" "$2" # exit status of quiet grep is returned } # assert $1 contains $2 assertIncludes() { - assertTrue "'$1' should have contained '$2'" $(_includes "$@") + _includes "$@" + local grep_exit=$? + assertTrue "'$1' should have contained '$2'" '[[ $grep_exit == 0 ]]' } + # assert $1 does not contain $2 assertNotIncludes() { - assertFalse "'$1' should not have contained '$2'" $(_includes "$@") + _includes "$@" + local grep_return=$? + assertTrue "'$1' should not have contained '$2'" '[[ ! $grep_exit = 0 ]]' }