Fish shell - advanced control flow - shell

Normally, fish shell processes commands like this:
1 && 3 (2)
This is perfectly useful and it mirrors the order of execution that I would want most of the time.
I was wondering if a different syntax exists to get a slightly different order of execution?
Sometimes I want this:
2 && 3 (1)
is that possible without using multiple lines ?
This is a trivial example:
cd ~ && cat (pwd | psub)
In this example I want to run pwd first then run cd and then run cat
edit: oh! This seems to work:
cat (pwd | psub && cd ~)

This is one of those cases where I'm going to recommend just using multiple lines [0].
It's cleaner and clearer:
set -l dir (pwd)
cd ~ && cat (printf '%s\n' $dir | psub)
This is completely ordinary and straightforward, and that's a good thing. It's also easily extensible - want to run the cd only if the pwd succeded?
set -l dir (pwd)
and cd ~ && cat (printf '%s\n' $dir | psub)
as set passes on the previous $status, so here it passes on the status of pwd.
The underlying philosophy here is that fish script isn't built for code golf. It doesn't have many shortcuts, even ones that posix shell script or especially shells like bash and zsh have. The fish way is to simply write the code.
Your answer of
cat (pwd | psub && cd ~)
doesn't work because that way the cat is no longer only executed if the cd succeeds - command substitutions can fail. Instead the cd is now only done if the psub succeeded - notably this also happens if pwd fails.
(of course that cat (pwd | psub) is fairly meaningless and could just be pwd, I'm assuming you have some actual code you want to run like this)
[0]: Technically this doesn't have to be multiple lines, you can write it as set -l dir (pwd); cd ~ && cat (printf '%s\n' $dir | psub). I would, however, also recommend using multiple lines

Related

ack command didn't return output from current directory in bash script

(edited)
Let say i have some directory structure like this:
lv1_directory
| file_contain_word_something.txt
| lv2_directory
so now i'm at lv2_directory and i have a code like this:
#!/bin/bash
findfile=$(ack -n 'something' . | wc -l)
cd ..
ls
echo $findfile
when i run the script it give me
lv2_directory file_contain_word_something.txt
0
but if i didn't assign it to variable it work like charm
#!/bin/bash
cd ..
ls
ack -n 'something' | wl -l
it give me
lv2_directory file_contain_word_something.txt
1
so i have to change it to this to work
#!/bin/bash
findfile=$(ack -n 'something' .. | wc -l)
cd ..
ls
echo $findfile
it give me the result i want
lv2_directory file_contain_word_something.txt
1
How can i use the first script and give me the result i want?
I think the problem here is:
You are currently inside lv2_directory which DOES NOT HAVE any file which matches the string 'something'. So when you fire ack from this dir itself, you get 0 for number of lines. Then you do cd...
#!/bin/bash
findfile=$(ack -n 'something' . | wc -l)
cd ..
ls
echo $findfile
Now, in your next snippet:
#!/bin/bash
cd ..
ls
ack -n 'something' | wl -l
You are first doing cd, so you are into lv1_directory, which has the file file_contain_word_something.txt. Then you fire your ack. Now it finds something in the txt file and hence, gives you 1 as the output(assuming there's only 1 matching line).
Like you wrote your script, it will never list (ls instruction), and count (wc instruction) in the same directory, so you have no result guarantee.
In addition, according to the directory in which you will launch the script, it may not work at all.
May you consider managing path at beginning of your script (and don't modify it between your instructions)?
Do you exactly know where is it on the system?
An alternative, may be to create an environment variable (for instance in your ~/.bash_profile file), and to use it at beginning of your script, ensuring it'll always be run in the good directory.
after some experiment i can achieve the result i want using function
#!/bin/bash
function findfile(){
ack -n 'something' | wc -l
}
cd ..
findfile
and its gonna give you 1
thank you for answering my question

gnu parallel to parallelize a for loop

I have seen several questions about this topic, but I lack the ability to translate this to my specific problem. I have a for loop that loops through sub directories and then executes a .sh script on a compressed text file inside each directory. I want to parallelize this process, but I'm struggling to apply gnu parallel.
Here is my loop:
for d in ./*/ ; do (cd "$d" && script.sh); done
I understand I need to input a list into parallel, so i have been trying this:
ls -d */ | parallel cd && script.sh
While this appears to get started, I get an error when gzip tries to unzip one of the txt files inside the directory, saying the file does not exist:
gzip: *.txt.gz: No such file or directory
However, when I run the original for loop, I have no issues aside from it taking a century to finish. Also, I only get the gzip error once when using parallel, which is so weird considering I have over 1000 sub-directories.
My questions are:
How do I get Parallel to work in my case? How do I get parallel to parallelize the application of a .sh script to 1000s of files in their own sub-directories? ie- what is the solution to my problem? I gotta make progress.
What am I missing? Syntax, loop, bad script? I want to learn.
Is Parallel actually attempting to run all these .sh scripts in parallel? Why dont I get an error for every .txt.gz file?
Is parallel the best option for the application? Is there another option that is better suited to my needs?
Two problems:
In:
ls -d */ | parallel cd && script.sh
what is paralleled is just cd, not script.sh. script.sh is only executed once, after all parallel cd jobs have run, if there was no error. It is the same as:
ls -d */ | parallel cd
if [ $? -eq 0 ]; then script.sh; fi
You do not pass the target directory to cd. So, what is executed by parallel is just cd, which just changes the current directory to your home directory. The final script.sh is executed in the current directory (from where you invoked the command) where there are probably no *.txt.gz files, thus the error.
You can check yourself the effect of the first problem with:
$ mkdir /tmp/foobar && cd /tmp/foobar && mkdir a b c
$ ls -d */ | parallel cd && pwd
/tmp/foobar
The output of pwd is printed only once, even if you have more than one input directory. You can fix it by quoting the command and then check the second problem with:
$ ls -d */ | parallel 'cd && pwd'
/homes/myself
/homes/myself
/homes/myself
You should see as many pwd outputs as there are input directories but it is always the same output: your home directory. You can fix the second problem by using the {} replacement string that is substituted with the current input. Check it with:
$ ls -d */ | parallel 'cd {} && pwd'
/tmp/foobar/a
/tmp/foobar/b
/tmp/foobar/c
Now, you should have all input directories properly listed in the output.
For your specific problem this should work:
ls -d */ | parallel 'cd {} && script.sh'

changing directories by using cd

I executed the following command :
cd /mnt/c/Users/Daniel/Documents/Assg/ | cat file.txt
my question is why doesn't it change directory?. The output file.txt is displayed but the directory is not changed. I understand that if we execute the same command in the following order, it won't work because cd changes directory in a child process, so the net result is the same.
cat file.txt | cd /mnt/c/Users/Daniel/Documents/Assg/
Try just cd /mnt/c/Users/Daniel/Documents/Assg/
As was already stated, the following:
cd /mnt/c/Users/Daniel/Documents/Assg/
should do the trick, but I'd like to go a bit more into why the command you presented doesn't work as expected. In Bash (and other shells), you can have multiple "subshells" running under a parent shell. each of these subshells has its own working directory. When you run commands in a pipeline, as you have done, a subshell is created. The working directory of the subshell was changed, but that didn't have any effect on the shell you were working in.
It depends on the shell you use
When you run two commands in a pipeline, typically one or both of the commands is run in a separate child process.
In older shells this would be both, in later shells this can be either
the first or the last.
At one point, the ksh93 team decided to make the last command in the pipeline the parent. This would prevent race conditions, and if the command was a builtin, it allows it to run inside the current shell
process and preserve the results of the pipeline.
Nevertheless, cd is a command that does not consume or produce any input or output (except for diagnostics on stderr), and using it in a pipeline
by itself is just silly. A better, because more predictable, command line would be:
cd /mnt/c/Users/Daniel/Documents/Assg/ && cat file.txt
This will assure that cat only runs if cd succeeds, and will then
show the contents of file.txt from the given directory.
You have different options.
Perform cat after trying to change dir
cd /mnt/c/Users/Daniel/Documents/Assg/ ; cat file.txt
Perform cat only when change dir worked
cd /mnt/c/Users/Daniel/Documents/Assg/ && cat file.txt
Perform cat in the other directory, but return to the current dir when finished.
(cd /mnt/c/Users/Daniel/Documents/Assg/ && cat file.txt)
# or
cat /mnt/c/Users/Daniel/Documents/Assg/file.txt
EDIT:
Your question: "why doesnt cd /mnt/c/Users/Daniel/Documents/Assg/ | cat file.txt, change directory?." can be answered two ways.
The technical explanation is given by #Henk (The pipe introduces a subshell, and environ settings in a subshell get lost when the shell exits).
The functional explanation is that you used the wrong syntax for what you are trying to accomplish.

change terminal title according to the last 2 directories in the path

I want to change the title everytime I enter a new directory ( when using cd ), but show only the last 2 directories. I'm using tcsh at work and bash at home.
For example: if I'm at folder ~/work/stuff and I write: cd 1.1, I want my new title to be stuff/1.1.
I already know how to change the title everytime I change the folder:
alias cd 'cd \!*; echo "\033]0;`pwd`\a"'
And I know how to take only the 2 last directories:
pwd | awk -F / -v q="/" '{print $(NF-1)q$NF}'
The question is how to combine these two, or how to do it in a different way?
It doesn't have to be through alias to cd.
What I did was creating a script file named titleRename.tcsh with the following code:
#!/bin/tcsh -f
set fullpath = "`pwd`\a"
set newTitle = `echo $fullpath | awk -F / '{OFS="/"; if(NF > 2){printf $(NF-2);printf "/"; printf $(NF-1); printf "/"; print $NF;}else print $0}'`
echo "\033]0;$newTitle"
It splits the pwd with awk, getting only the last 3 directories, and then it prints to the tab name.
Then I added in the .alias file the following:
alias cd 'cd \!*; <dir of script file>/titleRename.tcsh'
Now the title name changes automatically whenever I cd to a different directory :)
I originally thought you should be able to use the full command where you have pwd in backticks in the alias ie:
alias cd 'cd \!*; echo "\033]0;`pwd | awk -F / -v q="/" '{print $(NF-1)q$NF}'`\a"'
but now I realise there may be problems with the nested quoting. And that wouldn't work in bash anyway; I don't think there's a way to access command parameters in an alias.
Instead of aliasing cd you should update the title with the prompt. I don't know tcsh, but in bash the normal way to do this sort of thing is with the special pseudo-variable PS1:
# Minimalist prompt
PS1="\$ "
# Additionally set terminal title to cwd
case "$TERM" in
xterm*|rxvt*)
PROMPT_DIRTRIM=2
PS1="\033]0;\w\a$PS1"
;;
*)
;;
esac
That won't trim the directory name quite the way you were doing it, but unfortunately I can't get the quoting right to be able to use the escape sequence in PROMPT_COMMAND.

Is there a Bash shortcut for traversing similar directory structures?

The KornShell (ksh) used to have a very useful option to cd for traversing similar directory structures; e.g., given the following directories:
/home/sweet/dev/projects/trunk/projecta/app/models
/home/andy/dev/projects/trunk/projecta/app/models
Then if you were in the /home/sweet... directory then you could change to the equivalent directory in andy's structure by typing
cd sweet andy
So if ksh saw 2 arguments then it would scan the current directory path for the first value, replace it with the second and cd there. Is anyone aware of similar functionality built into Bash? Or if not, a hack to make Bash work in the same way?
Other solutions offered so far suffer from one or more of the following problems:
Archaic forms of tests - as pointed out by Michał Górny
Incomplete protection from directory names containing white space
Failure to handle directory structures which have the same name used more than once or with substrings that match: /canis/lupus/lupus/ or /nicknames/Robert/Rob/
This version handles all the issues listed above.
cd ()
{
local pwd="${PWD}/"; # we need a slash at the end so we can check for it, too
if [[ "$1" == "-e" ]]
then
shift
# start from the end
[[ "$2" ]] && builtin cd "${pwd%/$1/*}/${2:-$1}/${pwd##*/$1/}" || builtin cd "$#"
else
# start from the beginning
[[ "$2" ]] && builtin cd "${pwd/\/$1\///$2/}" || builtin cd "$#"
fi
}
Issuing any of the other versions, which I'll call cdX, from a directory such as this one:
/canis/lupus/lupus/specimen $ cdX lupus familiaris
bash: cd: /canis/familiaris/lupus/specimen: No such file or directory
fails if the second instance of "lupus" is the one intended. In order to accommodate this, you can use the "-e" option to start from the end of the directory structure.
/canis/lupus/lupus/specimen $ cd -e lupus familiaris
/canis/lupus/familiaris/specimen $
Or issuing one of them from this one:
/nicknames/Robert/Rob $ cdX Rob Bob
bash: cd: /nicknames/Bobert/Rob: No such file or directory
would substitute part of a string unintentionally. My function handles this by including the slashes in the match.
/nicknames/Robert/Rob $ cd Rob Bob
/nicknames/Robert/Bob $
You can also designate a directory unambiguously like this:
/fish/fish/fins $ cd fish/fins robot/fins
/fish/robot/fins $
By the way, I used the control operators && and || in my function instead of if...then...else...fi just for the sake of variety.
cd "${PWD/sweet/andy}"
No, but...
Michał Górny's substitution expression works nicely. To redefine the built-in cd command, do this:
cd () {
if [ "x$2" != x ]; then
builtin cd ${PWD/$1/$2}
else
builtin cd "$#"
fi
}

Resources