Bash script: only echo line to ~/.bash_profile once if the line doesn't yet exist - bash

I wrote a bash git-install script. Toward the end, I do:
echo "Edit ~/.bash_profile to load ~/.git-completioin.bash on Terminal launch"
echo "source ~/.git-completion.bash" >> ~/.bash_profile
The problem is, if you run the script more than once, you end up appending this line multiple times to ~/.bash_profile. How do I use bash scripting with grep or sed (or another option you may recommend) to only add the line if it doesn't yet exist in the file. Also, I want to add the line to ~/.profile if that file exists and ~/.bash_profile doesn't exist, otherwise just add it to ~/.bash_profile.

Something like this should do it:
LINE_TO_ADD=". ~/.git-completion.bash"
check_if_line_exists()
{
# grep wont care if one or both files dont exist.
grep -qsFx "$LINE_TO_ADD" ~/.profile ~/.bash_profile
}
add_line_to_profile()
{
profile=~/.profile
[ -w "$profile" ] || profile=~/.bash_profile
printf "%s\n" "$LINE_TO_ADD" >> "$profile"
}
check_if_line_exists || add_line_to_profile
A couple of notes:
I've used the . command instead of source as source is a bashism, but .profile may be used by non-bash shells. The command source ... is an error in .profile
I've used printf instead of echo because it's more portable and wont screw up backslash-escaped characters as bash's echo would.
Try to be a little more robust to non-obvious failures. In this case make sure .profile exists and is writable before trying to write to it.
I use grep -Fx to search for the string. -F means fixed strings, so no special characters in the search string needs to be escaped, and -x means match the whole line only. The -qs is common grep syntax for just checking the existence of a string and not to show it.
This is proof of concept. I didn't actually run this. My bad, but it's Sunday morning and I want to go out and play.

if [[ ! -s "$HOME/.bash_profile" && -s "$HOME/.profile" ]] ; then
profile_file="$HOME/.profile"
else
profile_file="$HOME/.bash_profile"
fi
if ! grep -q 'git-completion.bash' "${profile_file}" ; then
echo "Editing ${profile_file} to load ~/.git-completioin.bash on Terminal launch"
echo "source \"$HOME/.git-completion.bash\"" >> "${profile_file}"
fi

How about:
grep -q '^source ~/\.git-completion\.bash$' ~/.bash_profile || echo "source ~/.git-completion.bash" >> ~/.bash_profile
or in a more explicit (and readable) form:
if ! grep -q '^source ~/\.git-completion\.bash$' ~/.bash_profile; then
echo "Updating" ~/.bash_profile
echo "source ~/.git-completion.bash" >> ~/.bash_profile
fi
EDIT:
You should probably add an additional newline before your one-liner, just in case ~/.bash_profile does not end in one:
if ! grep -q '^source ~/\.git-completion\.bash$' ~/.bash_profile; then
echo "Updating" ~/.bash_profile
echo >> ~/.bash_profile
echo "source ~/.git-completion.bash" >> ~/.bash_profile
fi
EDIT 2:
This is a bit easier to modify and slightly more portable:
LINE='source ~/.git-completion.bash'
if ! grep -Fx "$LINE" ~/.bash_profile >/dev/null 2>/dev/null; then
echo "Updating" ~/.bash_profile
echo >> ~/.bash_profile
echo "$LINE" >> ~/.bash_profile
fi
The -F and -x options are specified by POSIX and were suggested in several other answers and comments.

# Decide which profile to add to
PROFILE=~/.bash_profile
if ! [ -e "$PROFILE" ] && [ -e ~/.profile ]; then
PROFILE=~/.profile
fi
# Add to profile if it doesn't appear to be there already. Err on the side of
# not adding it, in case user has made edits to their profile.
if ! grep -s 'git-completion\.bash' "$PROFILE"; then
echo "Editing $PROFILE to load ~/.git-completion.bash on Terminal launch"
echo "source ~/.git-completion.bash" >> "$PROFILE"
fi

Related

How can I remove the "empty space" in the terminal at launch?

I use iterm2 with oh-my-zsh (macOS) and I want start "terminal code" on the 1st line (now 5th line). Can you please tell me, How can I do this?
This is my ~/.zshrc:
export ZSH=/Users/ivbutsykin/.oh-my-zsh
ZSH_THEME="powerlevel9k/powerlevel9k"
POWERLEVEL9K_PROMPT_ADD_NEWLINE=true
plugins=(
zsh-autosuggestions
)
source $ZSH/oh-my-zsh.sh
test -e "${HOME}/.iterm2_shell_integration.zsh" && source "${HOME}/.iterm2_shell_integration.zsh"
local user_symbol="$"
if [[ $(print -P "%#") =~ "#" ]]; then
user_symbol = "#"
fi
POWERLEVEL9K_MULTILINE_LAST_PROMPT_PREFIX="%{%B%F{black}%K{yellow}%} $user_symbol%{%b%f%k%F{yellow}%} %{%f%}"
POWERLEVEL9K_VCS_MODIFIED_BACKGROUND='red'
echo -e "\033]6;1;bg;red;brightness;18\a"
echo -e "\033]6;1;bg;green;brightness;26\a"
echo -e "\033]6;1;bg;blue;brightness;33\a"
You can get rid of 3 empty lines by replacing echo -e with echo -ne. To get rid of the remaining empty line you need to upgrade to powerlevel10k.

terminal title not setting within screen

Currently, I'm setting terminal title within screen command, but the bash script gives me:
Cannot exec 'source /etc/profile && title.set root#test': No such file or directory
And I can run above command successful directly from the command line, here are my scripts:
/usr/local/bin/s
#!/bin/bash
if [ $1 ]
then
screen -D -R $1 -m "source /etc/profile && title.set `whoami`#$1"
else
screen -R
fi
/etc/profile
...
# Source global bash config
if test "$PS1" && test "$BASH" && test -z ${POSIXLY_CORRECT+x} && test -r /etc/bash.bashrc; then
. /etc/bash.bashrc
fi
function title.set() {
if [[ -z "$ORIG" ]]; then
ORIG=$PS1
fi
TITLE="\[\e]2;$*\a\]"
PS1=${ORIG}${TITLE}
}
# Termcap is outdated, old, and crusty, kill it.
unset TERMCAP
# Man is much better than us at figuring this out
unset MANPATH
...
So What's going wrong here?
The keyword source is a bash built-in command, i.e., something for which there is not necessarily an actual file to exec (another built-in command). You can only exec something that is a file — like bash, e.g., something like this:
screen -D -R $1 -m bash -c "source /etc/profile && title.set `whoami`#$1"

Read response and if then else loop

I have this piece of rudementary code in my .bash_profile that loads on login, but I can't get it working. Probably some easy fix, but I', staring my self blind on it right now.
The code:
# Simple backup when editing files with nano
function bu() {
read -p "Backup >>"`basename $1`"<< b4 edit [Y/n]?" response
echo $response
response=$response${response,,} # tolower
if [[ $response =~ ^(yes|y| ) ]]; then
mkdir -p ~/.backup
#cp -v "$1" ~/.backup/`basename $1`-`date +%Y%m%d%H%M`.backup
cp "$1" ~/.backup/`basename $1`-`date +%Y%m%d%H%M`.backup
echo ~/.backup/`basename $1`-`date +%Y%m%d%H%M`.backup >> ~/.backup/bu_log.txt
nano "$1"
else
nano "$1"
fi
}
And it has an alias nano="bu"
so, when i write nano, it should ask me if i want to backup the file first (on yes) or just open it in nano straight away.
The only thing that happens now is that it keeps asking the question and looping, newer goes to nano.
CentOS is the linux
Since nano is an alias to bu, typing nano runs your function, which calls nano, which is an alias to bu, which calls nano, ...
In order to run the actual nano editor, you need to disable alias expansion for that call. Use the command built-in:
command nano "$1"
You are calling nano recursivly, since you aliased nano=bu
so try to change the line in the script
nano "$1"
to the full path of
/usr/bin/nano "$1"
(or where nano is installed on your system)
I think you want:
response=${response,,}
You have
response=$response${response,,}
which gets you response=Yy. That won't match your regular expression.
You could also just do shopt -s nocasematch.
Aliases are usually trouble. The rule is "if in doubt, use a function."
nano() {
bu "$#"
}
bu() ( # Use a subshell to avoid having to reset shell options
shopt -s nocasematch
local base=${1##*/}
read -p "Backup >>${base}<< b4 edit [Y/n]?" response
case $response in
y*)
mkdir -p ~/.backup
local backup=~/.backup/"${base}-$(date +%Y%m%d%H%M`).backup"
cp "$1" "$backup"
echo "$backup" >> ~/.backup/bu_log.txt
;;
esac
command nano "$#" # Use "$#" to allow you to pass more than one argument to nano
)

How to get shell to self-detect using zsh or bash

I've a question on how to tell which shell the user is using. Suppose a script that if the user is using zsh, then put PATH to his .zshrc and if using bash should put in .bashrc. And set rvmrc accordingly.
#!/usr/bin/env bash
export PATH='/usr/local/bin:$PATH' >> ~/.zshrc
source ~/.zshrc
I've tried the following but it does not work : (
if [[ $0 == "bash" ]]; then
export PATH='/usr/local/bin:$PATH' >> ~/.bashrc
elif [[ $0 == "zsh" ]]; then
export PATH='/usr/local/bin:$PATH' >> ~/.zshrc
fi
# ... more commands ...
if [[ $0 == "bash" ]]; then
[[ -s '/Users/`whoami`/.rvm/scripts/rvm' ]] && source '/Users/`whoami`/.rvm/scripts/rvm' >> ~/.bashrc
source ~/.bashrc
elif [[ $0 == "zsh" ]]; then
[[ -s '/Users/`whoami`/.rvm/scripts/rvm' ]] && source '/Users/`whoami`/.rvm/scripts/rvm' >> ~/.zshrc
source ~/.zshrc
fi
If the shell is Zsh, the variable $ZSH_VERSION is defined. Likewise for Bash and $BASH_VERSION.
if [ -n "$ZSH_VERSION" ]; then
# assume Zsh
elif [ -n "$BASH_VERSION" ]; then
# assume Bash
else
# assume something else
fi
However, these variables only tell you which shell is being used to run the above code. So you would have to source this fragment in the user's shell.
As an alternative, you could use the $SHELL environment variable (which should contain absolute path to the user's preferred shell) and guess the shell from the value of that variable:
case $SHELL in
*/zsh)
# assume Zsh
;;
*/bash)
# assume Bash
;;
*)
# assume something else
esac
Of course the above will fail when /bin/sh is a symlink to /bin/bash.
If you want to rely on $SHELL, it is safer to actually execute some code:
if [ -n "$($SHELL -c 'echo $ZSH_VERSION')" ]; then
# assume Zsh
elif [ -n "$($SHELL -c 'echo $BASH_VERSION')" ]; then
# assume Bash
else
# assume something else
fi
This last suggestion can be run from a script regardless of which shell is used to run the script.
Just do echo $0
it says -zsh if it's zsh and -bash if it's bash
EDIT: Sometimes it returns -zsh and sometimes zsh and the same with bash, idk why.
A word of warning: the question you seem to have asked, the question you meant to ask, and the question you should have asked are three different things.
“Which shell the user is using” is ambiguous. Your attempt looks like you're trying to determine which shell is executing your script. That's always going to be whatever you put in the #! line of the script, unless you meant your users to edit that script, so this isn't useful to you.
What you meant to ask, I think, is what the user's favorite shell is. This can't be determined fully reliably, but you can cover most cases. Check the SHELL environment variable. If it contains fish, zsh, bash, ksh or tcsh, the user's favorite shell is probably that shell. However, this is the wrong question for your problem.
Files like .bashrc, .zshrc, .cshrc and so on are shell initialization files. They are not the right place to define environment variables. An environment variable defined there would only be available in a terminal where the user launched that shell and not in programs started from a GUI. The definition would also override any customization the user may have done in a subsession.
The right place to define an environment variable is in a session startup file. This is mostly unrelated to the user's choice of shell. Unfortunately, there's no single place to define environment variables. On a lot of systems, ~/.profile will work, but this is not universal. See https://unix.stackexchange.com/questions/4621/correctly-setting-environment and the other posts I link to there for a longer discussion.
You can simply try
echo $SHELL
the other answers fail with set -u
if [ ! -z ${ZSH_VERSION+x} ]; then
echo "this is zsh"
echo ${(%):-%x}
elif [ ! -z ${BASH_VERSION+x} ]; then
echo "this is bash"
echo $BASH_SOURCE
else
echo "not recognized"
fi
An alternative, might not work for all shells.
for x in $(ps -p $$)
do
ans=$x
done
echo $ans
Myself having a similar problem, settled for:
_shell="$(ps -p $$ --no-headers -o comm=)"
if [[ $_shell == "zsh" ]]; then
read -q -s "?Do it?: "
fi
elif [[ $_shell == "bash" || $_shell == "sh" ]]; then
read -n 1 -s -p "Do it [y/n] "
fi
Here is how I am doing it based on a previous answer from Gilles :
if [ -n "$ZSH_VERSION" ]; then
SHELL_PROFILE="$HOME/.zprofile"
else
SHELL_PROFILE="$HOME/.bash_profile"
fi
echo "export VAR1=whatever" >> $SHELL_PROFILE
echo "INFO: Refreshing your shell profile: $SHELL_PROFILE"
if [ -n "$ZSH_VERSION" ]; then
exec zsh --login
else
source $SHELL_PROFILE
fi

Can I specify redirects and pipes in variables?

I have a bash script that creates a Subversion patch file for the current directory. I want to modify it to zip the produced file, if -z is given as an argument to the script.
Here's the relevant part:
zipped=''
zipcommand='>'
if [ "$1" = "-z" ]
then
zipped='zipped '
filename="${filename}.zip"
zipcommand='| zip >'
fi
echo "Creating ${zipped}patch file $filename..."
svn diff $zipcommand $filename
This doesn't work because it passes the | or > contained in $zipcommand as an argument to svn.
I can easily work around this, but the question is whether it's ever possible to use these kinds of operators when they're contained in variables.
Thanks!
I would do something like this (use bash -c or eval):
zipped=''
zipcommand='>'
if [ "$1" = "-z" ]
then
zipped='zipped '
filename="${filename}.zip"
zipcommand='| zip -#'
fi
echo "Creating ${zipped}patch file $filename..."
eval "svn diff $zipcommand $filename"
# this also works:
# bash -c "svn diff $zipcommand $filename"
This appears to work, but my version of zip (Mac OS X) required that i change the line:
zipcommand='| zip -#'
to
zipcommand='| zip - - >'
Edit: incorporated #DanielBungert's suggestion to use eval
eval is what you are looking for.
# eval 'printf "foo\nbar" | grep bar'
bar
Be careful with quote characters on that.
Or you should try zsh shell whic allows to define global aliases, e.g.:
alias -g L='| less'
alias -g S='| sort'
alias -g U='| uniq -c'
Then use this command (which is somewhat cryptic for the ones who took a look from behind ;-) )
./somecommand.sh S U L
HTH
Open a new file handle on either a process substitution to handle the compression or on the named file. Then redirect the output of svn diff to that file handle.
if [ "$1" = "-z" ]; then
zipped='zipped '
filename=$filename.zip
exec 3> >(zip > "$filename")
else
exec 3> "$filename"
fi
echo "Creating ${zipped}patch file $filename"
svn diff >&3

Resources