inaccurate [ -L $location ] or [ -f $location ] when location="~/.bashrc" - variables

It's my first time doing something with bash-programming. As a first example I'm trying to source my .bashrc from my .bash_profile - even when ~/.bashrc is a symbolic link.
.bash_profile:
if [ -f ~/.bashrc ] && ! [ -L ~/.bashrc ]
then
# ~/.bashrc is a regular file. Source it!
source ~/.bashrc
echo "~/.bashrc found."
elif [ -L ~/.bashrc ]
then
# ~/.bashrc is a symbolic link.
# Recursivly follow symbolic links.
location="~/.bashrc"
while [ -L $location ]
do
# QUESTION: Control-Flow never reaches this point.
# Follow link on macOS.
location="$(readlink $path)"
done
# Check if final target is regular file. Source it!
if [ -f $location ]
then
source $location
echo "Symlink to .bashrc found."
fi
else
echo "No valid .bashrc found."
fi
This is what I expect my code to do:
If ~/.bashrc is not a symbolic link, but a regular file: Source it.
Otherwise if ~/.bashrc is a symbolic link:
Follow the symbolic link as long as the target keeps being a symbolic link.
If the final target is a regular file: Source it.
Otherwise: Give it up.
As a test I created a symbolic link ~/.bashrc to the original file .dotfiles/.bashrc. My code enters the elif as intended, but sadly never enters the body of the while-loop (as I would expect, since ~/.bashrc is a symbolic link).
What is going on here? I think the variable assignment of location is wrong in some way.

Replace:
location="$(readlink $path)"
With:
location="$(readlink $location)"
Notes:
The variable path was never defined. I believe that you intended to apply readlink to location instead
If you had GNU readlink (available on Linux, homebrew, etc), then the option -f could be used eliminating the need for a loop.
In general, shell variables should be referenced inside double-quotes unless one wants the shell to apply word splitting, tilde expansion, and pathname expansion to the value of the variable.
For example, in the following line, we want tilde expansion. So, ~/ needs to be outside of quotes:
location=~/.bashrc
Once this is done, the parts of the script where location is referenced should be in double-quotes. As one example:
location="$(readlink "$location")"
This becomes very important, for example, if file or path names contains spaces.

This causes the problem:
location="~/.bashrc"
The tilde expansion doesn't happen in double quotes, and it doesn't happen in
[ -L $location ]
either.
So, don't use double quotes in the assignment, or use
location="$HOME/.bashrc"
or similar.

Related

Unable to override PS1 with direnv

I am following the direnv wiki on PS1. I have the following relevant entries in my files.
.bashrc
DEFAULT_PS1='\[$(ppwd)\]\u#\h:\w$(__git_ps1 " (%s)")'
# add some more things to DEFAULT_PS1, conditionally
DEFAULT_PS1+='> '
PS1=${CUSTOM_PS1:-$DEFAULT_PS1}
# optional bashrc file extensions
for f in ~/.bashrc_*; do test -s $f && . $f || true; done
eval "$(direnv hook bash)"
.envrc
export KUBECONFIG=~/.config/kube/homelab.yaml
export KUBE_PS1_ENABLED=on
export CUSTOM_PS1='$(kube_ps1) $ '
PATH_add scripts
I have allowed the latest version of the .envrc with direnv allow. However, when changing to the directory, the custom PS1 value is not set, although the values seems to be right
$ cd -
/home/robert/sources/oss/sling-cloud-native
direnv: loading .envrc
direnv: export +CUSTOM_PS1 +KUBE_PS1_ENABLED ~KUBECONFIG ~PATH
$ echo $PS1
\[$(ppwd)\]\u#\h:\w$(__git_ps1 " (%s)")$(kube_ps1)>
$ echo $CUSTOM_PS1
$(kube_ps1) $
I am not sure how the solution in the wiki is supposed to work, as apparently the value of PS1 is set to the DEFAULT_PS1 when the .bashrc file is loaded the first time and is not re-evaluated as part of the direnv hook.
How can I change the value of PS1 using direnv?
The direnv wiki mentions that the author had to "blacklist PS1 as an environment variable that can be changed," mainly because "The core issue is that PS1 is a local variable." So I don't think workarounds that involve using the .envrc file to indirectly modify the PS1 can work.
I had a similar issue with python virtual environments, which I realize is different to your use case, but there is an example in this blog that could be helpful.
Because links can die I reproduce it here:
add the following to ~/.bashrc (me: I tested this with ~/.zshrc and it also works)
show_virtual_env() {
if [[ -n "$VIRTUAL_ENV" && -n "$DIRENV_DIR" ]]; then
echo "($(basename $VIRTUAL_ENV))"
fi
}
export -f show_virtual_env
PS1='$(show_virtual_env)'$PS1
Then source the file again
source ~/.bashrc
The wiki also mentions adding unset PS1 to the .envrc file, which removes any error about direnv: PS1 cannot be exported... and I can confirm that also works with this scenario.
Perhaps you can do something similar; use .envrc to export the environment variables as you are doing, but remove the line export CUSTOM_PS1='$(kube_ps1) $ ' and in your ~/.bashrc make a function that checks if you have set KUBE_PS1_ENABLED and appends '$(kube_ps1) $ ' to PS1 if it is set.

Adding home-brew to PATH

I just installed Home-brew and now I'm trying to insert the home-brew directory at the top of my path environment variable by typing in two commands inside my terminal. My questions are these:
What is a path environment variable?
Are the two codes provided me correct?
echo "export Path=/usr/local/bin:$PATH" >> ~/.bash_profile && source ~/.bash_profile
After this I am to type in brew doctor. Nothing is happening as far as I can see.
Can anyone offer me some advice or direction?
I installed brew in my new Mac M1 and ask me to put /opt/homebrew/bin in the path, so the right command for this case is:
echo "export PATH=/opt/homebrew/bin:$PATH" >> ~/.bash_profile && source ~/.bash_profile
TL;DR
echo "export PATH=/usr/local/bin:$PATH" >> ~/.bash_profile && source ~/.bash_profile
is what you want.
To answer your first question; in order to run (execute) a program (executable) the shell must know exactly where it is in your filesystem in order to run it. The PATH environment variable is a list of directories that the shell uses to search for executables. When you use a command that is not built into the shell you are using the shell will search through these directories in order and will execute the first matching executable it finds.
For example when you type: mv foo bar the shell is almost certainly actually using an executable located in the /bin directory. Thus fully the command is
/bin/mv foo bar
The PATH environment variable therefore saves you some extra typing. You can see what is in your PATH currently (as you can with all environment variables) by entering:
echo $<NAME OF VARIABLE>
So in this instance:
echo $PATH
As I mentioned earlier, ordering is important. Adding /usr/local/bin to the beginning of PATH means that the shell will search there first and so if you have an executable foo in that folder it will be used in preference to any other foo executables you may have in the folders in your path. This means that any executables you install with brew will be used in preference to the system defaults.
On to your second question. What the command you have provided is trying to do is add a line to your .bash_profile and then source it. The .bash_profile is a text file stored in your home directory that is sourced (read) every time bash (your shell) starts. The mistake in the line you've provided is that only the first letter of PATH is capitalised. To your shell Path and PATH are very different things.
To fix it you want:
echo "export PATH=/usr/local/bin:$PATH" >> ~/.bash_profile && source ~/.bash_profile
To explain
echo "export PATH=/usr/local/bin:$PATH"
simply prints or echoes what follows to stdout, which in the above instance is the terminal. (stdout, stderr and stdin are very important concepts on UNIX systems but rather off topic) Running this command produces the result:
export PATH=/usr/local/bin:/opt/local/sbin:/opt/local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin
on my system because using $PATH within double quotes means bash will substitute it with its value. >> is then used to redirect stdout to the end of the ~/.bash_profile file. ~ is shorthand for your home directory. (NB be very careful as > will redirect to the file and overwrite it rather than appending.)
&& means run the next command is the previous is successful and
source ~/.bash_profile
simply carries out the actions contained in that file.
As per the latest documentation, you need to do this:
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> /home/dhruv/.bashrc
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
Now you should be able to run brew from anywhere.
When you type in a program somewhere and click enter, it checks certain locations to see if that program exists there.
Linux brew uses locations different from the normal linux programs, so we are adding these locations to the ~/.profile file which sets the paths.
Run this in your terminal, and it will place the correct code in the .profile file, automatically.
echo "eval \$($(brew --prefix)/bin/brew shellenv)" >>~/.profile
Don't use .bash_profile because when you use something different from bash, like zsh, it may not work. .profile is the correct location.

How can you compile all cpp files in a directory?

I have a number of source files in a number of folders.. Is there a way to just compile all of them in one go without having to name them?
I know that I can say
g++ -o out *.cpp
But when I try
g++ -o out *.cpp folder/*.cpp
I get an error.
What's the correct way to do this? I know it's possible with makefiles, but can it be done with just straight g++?
By specifying folder/*.cpp you are telling g++ to compile cpp files in folder. That is correct.
What you may be missing is telling the g++ where to locate additional files that those cpp files #include.
To do this, tell your compiler to also include that directory with -I like this:
g++ -o out -I ./folder *.cpp folder/*.cpp
In some circumstances I have had the compiler forget what was in the root/current directory, so I manually specified it with another -I to the current directory .
g++ -o out -I . -I ./folder *.cpp folder/*.cpp
Figured it out! :) I hate the idea of having to use make or a build system just to compile my code, but I wanted to split up my code into subfolders to keep it clean and organized.
Run this (replace RootFolderName (e.g. with .) and OutputName):
g++ -g $(find RootFolderName -type f -iregex ".*\.cpp") -o OutputName
The find command will do a recursive case-insensitive regex search (-iregex) for files (-type f). Placing this within $() will then inject the results into your g++ call. Beautiful! :)
Of course, make sure you're using the command right; you can always do a test run.
For those using Visual Studio Code to compile, I had to do this in the tasks.json args: [] of a task with "command": "g++":
"-g",
"$(find",
"code",
"-type",
"f",
"-iregex",
"'.*\\.cpp')",
"-o",
"program"
(Otherwise it would wrap the $() all in single quotes.)
(Thanks to: user405725 # https://stackoverflow.com/a/9764119/1599699 comments)
So I ran across this and saw the vscode task above, and managed a different solution. This will get all the headers and c files from the same directory. Then will work with the F5 key.
tasks.json
{
"tasks": [
{
"type": "shell",
"label": "C/C++: gcc-7 build active file",
"command": "/usr/bin/gcc-7",
"args": [
"-I",
"${fileDirname}",
"-g",
"${fileDirname}/*.c",
"-o",
"${fileDirname}/${fileBasenameNoExtension}"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
}
}
],
"version": "2.0.0"
}
In case that all files contain their own main and there are not part of the same project/program, you might want to keep their original name and create one executable for each file. If this is the case, you could use awk:
awk '{n=split(FILENAME, a, "."); outfile=sprintf("%s.out",a[n-1]); command=sprintf("g++ -I . %s -o %s", FILENAME, outfile); system(command); nextfile } ' *.cpp
For start, you use split to separate filename and extension and store the result into an array a. The basename is located at index n-1 and the extension is at index n. So, we change the extension into .out and by using sprintf store into the outfile variable. In the same way, we create the command to be executed by system function in a shell.
A shortest version of the above command is:
awk '{n=split(FILENAME, a, "."); command=sprintf("g++ -I . %s -o %s.out", FILENAME, a[n-1]); system(command); nextfile } ' *.cpp
You could also modify the command to #include additional files with -I as suggested by #Bit Fracture.
NOTE: This solution doesn't work well if any filename contains a space or multiple dots. Before executing the command, those characters need to be replaced with an underscore for example, as described here:
find . -type f -name "* *.cpp" -exec bash -c 'mv "$0" "${0// /_}"' {} \;

Shell redirection operation >|?

What is the difference between these two redirections?
[localhost ~]$ echo "something" > a_file.txt
[localhost ~]$ echo "something" >| a_file.txt
I can't seem to find any documentation about >| in the help.
>| overrides the noclobber option in the shell (set with $ set -o noclobber, indicates that files can not be written over).
Basically, with noclobber, you get an error if you try to overwrite an existing file using >:
$ ./program > existing_file.txt
bash: existing_file.txt: cannot overwrite existing file
$
Using >| will override that error and force the file to be written over:
$ ./program >| existing_file.txt
$
It's analogous to using the -f or --force option on many shell commands.
From the Bash Reference Manual Section "3.6.2 Redirecting Output":
If the redirection operator is >, and the noclobber option to the set builtin has been enabled, the redirection will fail if the file whose name results from the expansion of word exists and is a regular file. If the redirection operator is >|, or the redirection operator is > and the noclobber option is not enabled, the redirection is attempted even if the file named by word exists.
Searching for "bash noclobber" generally brings up articles that mention this somewhere. See this question on SuperUser, this section in O'Reilly's "Unix Power Tools", and this Wikipedia article on Clobbering for examples.

How do you get a custom Gnu screen config to load .bash_profile and .bash_aliases?

I have a custom screen configuration myscreenconfig and a .screenrc. myscreenconfig looks like this:
source .screenrc
screen 0 bash
title 'notes'
screen 1 bash
title 'bash'
[etc.]
.screenrc has these lines at the top:
altscreen on
shell -${SHELL}
My .bash_profile file sets a lot of things and then calls source $HOME/.bash_aliases.
If I start screen without any arguments, my .bash_profile gets loaded and .bash_aliases gets loaded. But if I start screen via screen -c myscreenconfig, only .bash_profile gets loaded, and not .bash_aliases. Why? How can I fix this?
What worked for me was making a symbolic link between wherever I had my bash settings and .bashrc (which I did not have):
ln -s ~/.bash_profile ~/.bashrc
I had the same problem on one of the machines I use. After reading the suggestion above about linking the two bash resource files, I realized that the following section had been put in comment in the .bash_profile file on this particular machine:
# Get the aliases and functions
# if [ -f ~/.bashrc ]; then
# . ~/.bashrc
# fi
After removing the comment signs (#) from before the if block lines, settings in .bashrc became available in screen sessions as well.
Because you are not using login shells in myscreenconfig. Use (IIRC) screen 0 -bash, or try combinations with deflogin on.
I'm use this in my .bashrc
if [ "$TERM" = "screen" ]; then
if [ -f ~/.bash_profile ]; then
. ~/.bash_profile
fi
fi