zshrc alias gets executed when launching a new terminal - zshrc

I am using zsh and in my config I am adding another alias :
alias recursively_git_pull_all_repo="for dir in $(find . -name ".git"); do cd ${dir%/*}; git pull ; cd -; done"
However this alias seems to get executed every time I open a new terminal (or at least it slows down starting up a new terminal considerably).
How can I add this alias without it being launched every time I open a new terminal ?

TL;DR (or should it be TL;WR - "too long, won't read"?)
alias recursively_git_pull_all_repo='for dir in **/.git(/:h); git -C $dir pull'
You are at least partially correct in that a part of this command is run when the alias is declared. This has two effects:
Loading the alias takes time
The alias does not work
Explanation
You are declaring the alias in double quotes, which allows for parameter expansion. That means that the parts $(find . -name ".git") and ${dir%/*} will be expanded and substituted at the time the alias is declared taking the values they produce at the time.
For $(find . -name ".git") this means that . is most likely $HOME and the construct is replaced by a newline separated list of all .git directories (and other file-like objects) in your home directory.
${dir%/*} will probably be substituted with an empty string as dir is most likely not set. Remember: the alias itself is not executed at that time, so dir will not be set by the for loop.
This means that:
alias recursively_git_pull_all_repo="for dir in $(find . -name ".git"); do cd ${dir%/*}; git pull ; cd -; done"
will effectively be saved as something like
alias recursively_git_pull_all_repo="for dir in docs/repo1/.git
docs/repo2/.git
otherdocs/repo/.git
whatever/.git; do
cd ; git pull ; cd -; done"
This will - fortunately - fail with an zsh: command not found: docs/repo2/.git error because of the newline after the first repository. If it did not, it would change into your home directory repeatedly (cd without parameter) and try to do git pull.
Solution
The quick solution would be to just use single quotes, when declaring the alias, that way the command and parameter will substituted when the alias is run, not when it is declared.
But there are still some other problems with that:
if any of the directory names contain white spaces, this will fail because for will interpret it as two values
if you cannot switch into one of the found directories, git pull will be run from the current directory instead. This can actually happen if the execute flag is not set on a directory in the path.
without additional parameters find will also list non-directories named .git
Instead I would suggest to use what zsh has to offer:
alias recursively_git_pull_all_repo='for dir in **/.git(/:h); git -C $dir pull'
you do not actually need find to get a list of the directories. The ** glob will be recursively expanded to all directories. So **/.git will be expanded to all paths with .git as last element.
you can use glob qualifiers to restrict expansions to elements that fulfill certain criteria. In this case **/.git(/) the qualifier / means: "only directories".
you can also history expansion modifiers as glob qualifiers. This is done with a : followed by the modifier - in this case h, which removes the last path component (like the program dirname, or ${dir%/*} from your example).
you do not actually need do change directory into a git repository in order to do use git; you can tell git where to work with git -C <path>
as you are now only running one command inside the loop, you can use the short syntax for for, meaning no do ...; done.
Fun fact: If you use d instead of dir, another short form of for and the option GLOB_STAR_SHORT is enabled (requires zsh >= 5.2), you can use:
for d (**.git(/:h))git -C $d pull
which is only 4 characters longer than your alias name. Yes, I know about completion. But there is also history searching 😉.

You could put it in a function instead of an alias:
$ cat zshFunction
function recursively_git_pull_all_repo () {
echo "Executing code"
for dir in $(find . -name ".git"); do cd ${dir%/*}; git pull ; cd -; done
}
$ recursively_git_pull_all_repo
zsh: command not found: recursively_git_pull_all_repo
$ source zshFunction
$
$ recursively_git_pull_all_repo
Executing code
$

Related

Fish: for loop: Too many args for cd command

If I run a script
#!/usr/bin/fish
cd /home/user/repos/repo1
pwd
works as intended (changes directory and prints that path).
When I place the same logic into a for loop, cd complains about too many arguments even though there are no space and .
#! /usr/bin/fish
set REPOS "/home/user/repos/repo1" "/home/user/repos/repo1"
for REPO_PATH in $REPOS:
echo $REPO_PATH
cd $REPO_PATH
pwd
end
Using relative paths doesn't help,
set REPOS "~/repos/repo1" "~/repos/repo1"
using variables without quotes work outside for loop, same complaint inside for loop.
Works in bash like this
#!/bin/bash
repos=("/home/user/repos/repo1" "/home/user/repos/repo1")
for path in "${repos[#]}"
do
cd $path
pwd
done
I've read List and Loops section of fish documentation.
I've noticed that cd has a fish-wrapper. Could that be the issue?
Remove the colon.
The reason this shows the error "Too many arguments" is that calling a variable "...PATH" causes fish to interpret it as a path variable, which means that it will be split on ":" (like $PATH, $LD_LIBRARY_PATH etc).
So fish goes, sets $REPO_PATH to "/home/user/repos/repo1:", then splits it on the ":", making it a list of "/home/user/repos/repo1" and "".
That's then passed to cd as two arguments, one being empty, and cd complains about having a second spurious argument.
So just use
set REPOS "/home/user/repos/repo1" "/home/user/repos/repo1"
for REPO_PATH in $REPOS
echo $REPO_PATH
cd $REPO_PATH
pwd
end

Add portion of filepath as a prefix to a filename for all files in a folder

I'm trying to rename files based on the upstream directory name but I don't have any bash script experience.
Let's say I have a directory that contains thousands of directories named "k99_xxx_properties", and in each k99 directory there is an Alignments directory, and in the Alignments directory there are files named sample1, sample2, sample3 in and so on. What I want to do is rename the sample files by adding the "k99_xxx" portion of the upstream directory as a prefix ("k99_xxx_sample1").
I know all I need to do is loop through all k99_xxx_properties directories, save the k99_xxx portion of the directory name as a variable, and navigate into each k99_xxx, cd into Alignments, and loop through every file in Alignments to add k99_xxx as a prefix for the sample filenames.
How would I go about that in a bash script?
This Shellcheck-clean code is a straightforward translation of the description in the question into Bash:
#! /bin/bash -p
shopt -s nullglob # Globs that match nothing expand to nothing
# Loop through all k99_xxx_properties directories
for k99dir in k99_*_properties/ ; do
# Save the k99_xxx portion of the directory name as a variable
k999_xxx=${k99dir%_properties/}
# (Skip the directory if it doesn't have an 'Alignments' subdirectory)
[[ -d ${k99dir}Alignments ]] || continue
# navigate into each k99_xxx, cd into Alignments
(
cd -- "${k99dir}Alignments" || exit
# loop through every file in Alignments to add k99_xxx
# as a prefix for the sample filenames.
for sf in sample* ; do
echo mv -- "$sf" "${k999_xxx}_${sf}"
mv -- "$sf" "${k999_xxx}_${sf}"
done
)
done
shopt -s nullglob is to prevent the code trying to process spurious 'k99_*_properties' directories or 'sample*' files if it is run where they do not exist.
See Removing part of a string (BashFAQ/100 (How do I do string manipulation in bash?)) for an explanation of ${k99dir%_properties/}.
The parentheses around the statements starting at cd ... cause them to be run in a subshell. This means that the cd (and the possible exit) doesn't affect the current shell. The advantage is that there is no need to cd back to the starting directory for the next iteration of the outer loop. It is not always possible to cd back to a directory after using cd to leave it.
The conditional exit after cd (which only exits the surrounding (subshell-creating) parentheses) is to prevent trying to do file moves in the wrong directory if cd fails. Shellcheck complains if the exit is not present (which is one of the reasons why it's a good idea to always use Shellcheck on shell code).
Since it is executed in a directory different from the current one, the output of echo mv ... may be confusing. (This is one of the reasons why it's best to avoid using cd in programs. It's generally best to do operations from the current directory.)

How could I make a directory and navigate to the directory by only typing the name of the directory once using bash? [duplicate]

I'm searching for just one command — nothing with && or | — that creates a directory and then immediately changes your current directory to the newly-created directory. (This is a question someone got for his exams of "linux-usage", he made a new command that did that, but that didn't give him the points.) This is on a debian server if that matters.
I believe you are looking for this:
mkdir project1 && cd "$_"
define a bash function for that purpose in your $HOME/.bashrc e.g.
function mkdcd () {
mkdir "$1" && cd "$1"
}
then type mkdcd foodir in your interactive shell
So stricto sensu, what you want to achieve is impossible without a shell function containing some && (or at least a ; ) ... In other words, the purpose of the exercise was to make you understand why functions (or aliases) are useful in a shell....
PS it should be a function, not a script (if it was a script, the cd would affect only the [sub-] shell running the script, not the interactive parent shell); it is impossible to make a single command or executable (not a shell function) which would change the directory of the invoking interactive parent shell (because each process has its own current directory, and you can only change the current directory of your own process, not of the invoking shell process).
PPS. In Posix shells you should remove the functionkeyword, and have the first line be mkdcd() {
For oh-my-zsh users: take 'directory_name'
Reference: Official oh-my-zsh github wiki
Putting the following into your .bash_profile (or equivalent) will give you a mkcd command that'll do what you need:
# mkdir, cd into it
mkcd () {
mkdir -p "$*"
cd "$*"
}
This article explains it in more detail
I don't think this is possible but to all people wondering what is the easiest way to do that (that I know of) which doesn't require you to create your own script is:
mkdir /myNewDir/
cd !$
This way you don't need to write the name of the new directory twice.
!$ retrieves the last ($) argument of the last command (!).
(There are more useful shortcuts like that, like !!, !* or !startOfACommandInHistory. Search on the net for more information)
Sadly mkdir /myNewDir/ && cd !$ doesn't work: it retrieves the last of argument of the previous command, not the last one of the mkdir command.
Maybe I'm not fully understanding the question, but
>mkdir temp ; cd temp
makes the temp directory and then changes into that directory.
mkdir temp ; cd temp ; mv ../temp ../myname
You can alias like this:
alias mkcd 'mkdir temp ; cd temp ; mv ../temp ../'
You did not say if you want to name the directory yourself.
cd `mktemp -d`
Will create a temp directory and change into it.
Maybe you can use some shell script.
First line in shell script will create the directory and second line will change to created directory.

bash - how is * interpreted in aliases?

[Note - I solved the problem I ran into using a bash function, but I want to understand why my initial attempt didn't work.]
I run git in Windows, and use the Git Bash command line application to manage repositories.
I have several repositories that I often want to pull all at once. Previously I was doing this just by entering the following on the command line:
for i in *; do cd $i; git pull --rebase; cd ..; done;
To save time, I decided to create an alias for this. So, I created a .bashrc file in the home directory (C:/git in my case) and added the line
alias pr="for i in *; do cd $i; git pull --rebase; cd ..; done;"
However, this didn't work at all, the output was
sh.exe" cd: /etc/profile.d/*.sh: No such file or directory
followed by git complaining that it wasn't in a repository. The output would be repeated over 20 times for a single call. The root of the MinGW filesystem, where the /etc above derives from, is where I installed git to (C:/Program Files (x86)/Git).
Now, I solved this problem in the end by creating a function in my .bashrc file instead, like so:
pr(){
for i in *
do
cd $i
git pull --rebase
cd ..
done
}
So, my problem is solved, but I do want to understand why my initial approach didn't work. There's clearly something about aliases I don't understand that is, presumably, mangling the 'i in *' bit. My expectation was that bash would substitute the string the alias maps to and then evaluate it, but it doesn't seem to be that straightforward.
I haven't analyzed this in full, but it has to do with the time at which your $i in the alias gets expanded. The way you're doing this, $i will probably be initialized during alias creation. In the function version, it will be interpreted at run-time. Try defining the alias as you've shown, then run:
alias pr # show how the alias is defined
Bash will happily postpone expansion of $-variables if you define the alias with single quotes. The following should work as well as your function.
alias pr='for i in *; do cd $i; git pull --rebase; cd ..; done;'

Implementing autocompletion to zsh aliases

I am using the awesome zsh framework oh-my-zsh. Every day I hit gc which is an alias for git commit I hit <Tab><Tab> and it gives me the correct files to commit. I am really interested to know how that is implemented, so I can implement my own idea of navigating to sub-directories using an alias.
I mean navigating into ~/workspace/a-repo using this alias -w a<Tab><Tab> which completes to a-repo and gives me some suggestions which are folders inside ~/workspace. I made this alias already but I need to know how to add the correct autocompletion/suggestion to it.
Here is my alias:
-w() { cd ~/workspace/"$*" }
In the case of gc (which I assume is defined as alias gc='git commit -v' in the git plugin of oh-my-zsh) zsh internally substitutes the alias (gc) with the actual command (git commit -v ) before looking for trying for completions. This works for every alias (unless the shell option COMPLETE_ALIASES is set, which would allow to set separate completions for aliases).
As for what you want to do: Seing that -w is actually a function and not an alias, you indeed would have to write your own completion. But there is a much simpler way to go about it: Static named directories.
Named directories are usually just the home directories of the users on the system. The most commonly known is probably ~ for the home directory of the current user. Other users directories are named ~USERNAME. You can also define your own static named directories. There are two ways to do this:
Use hash -d to explicitly define a name, similar to an alias:
hash -d w=~/workspace
Implicitly define it by defining a string shell parameter whose value begins with an / (this also means you cannot use ~ as shortcut for your home directory)
w="${HOME}/workspace"
In both cases you now can use ~w to reference your workspace, in the second case you can also use $w (for example for use in quoted strings). Typing cd ~w/a-repo will get you to ~/workspace/a-repo. Tab-completion works like it would with any other path, so pressing ~w/a<Tab> will present you ~w/a-repo as completion.
Additionally, if you set the shell option AUTO_CD (setopt AUTO_CD), you can switch into a directory just by typing the path (or directory name) without the need for cd.
% w="/tmp"
% cd ~w
% pwd
/tmp
% mkdir 1 2 3
% setopt AUTO_CD
% ~w/<TAB>
1 2 3
% ~w/1
% pwd
/tmp/1

Resources