Getting line of onscreen output as parameter for command - linux

In bash (or zsh), is there a way to 'look back' on previous output?
Say I have the following:
$ find . -depth 1 -name "d*"
dir1
dir2
dir3
$ cd [3rd line]
Can I pluck 'dir3' from the previous output without repeating the previous command and using a pipe? Just sort of "grab it" from onscreen?

The shell doesn't see that output at all. When findruns, it uses the file handle that it inherits from the shell to write its output, but that isn't visible to the shell (though even if it were, the shell isn't caching it).
However, given that you are using zsh, the first question you should ask involving an external command is, "Do I even need the external command?" As is often the case, you don't here. Instead of using find, just use print and a glob to get the same listing:
% print -l d*
then use the same glob again with a qualifier to select the 3rd result.
% cd d*([3])
(This assumes that the expansion of d* won't change between running the print command and running the cd command.)
Or, you can cache the result yourself:
% choices=( d* )
% print -l $choices
dir1
dir2
dir3
% cd $choices[3]
The array-based solution could work in bash as well, though with slightly different syntax (and assuming your find command is simple enough to be replaced with one of bash's far less powerful globs.)
Finally, there is the select command:
select d in d*; do
cd "$d"; break;
done
which will also work as-is in bash, again assuming a valid glob.

A cursory search reveals neither bash nor zsh provide this functionality. Happy to be corrected if there's a way to do this. :)

Related

Linux shell convert list of nested files into json

I am developing on mac and use the following command to confirm file names to a json array:
ls **/*.test.json | jq -R -s -c 'split("\n")[:-1]'
Which gives me the json array:
['folder1/a.test.json', 'folder2/b.test.json', 'c.test.json']
Which is exactly what I want. However, when executing on github action (with linux), the above command produces outcome:
['c.test.json]
And the files within folders are not included.
I confirmed that the folders where checkout successfully because echo $(ls folder1) gives a.test.json.
What is the best way to achieve what I want for the command?
** is not a standard sh feature; your Github action probably requires a POSIX shell script.
To traverse arbitrarily deep directory structure with a shell which doesn't support **, try find:
find . -name '*.test.json' -print |
jq -R -s -c 'split("\n")[:-1]'
If there is only a limited set of directory levels, maybe try
printf '%s\n' */*.test.json *.test.json | jq ...
(Also don't use ls in scripts and perhaps see also useless use of echo.)
Both of these have some gnarly corner cases if you have file names which contain newlines; find could probably be coerced to handle that case, too, but I'll not complicate this answer further; perhaps see https://mywiki.wooledge.org/BashFAQ/020 for a fuller discussion.
If your shell is Bash, ** is available, but typically not enabled out of the box; you enable it with shopt -s globstar ... but perhaps it's better to stick to proper sh in case Github changes the default shell for Actions.

How to use case to identify a specific pattern in BASH script

I'm trying to identify several folders in my script that run from 101-121. The script is written to look through one specific folder at a time.
The error I get is:
command not found
Usage: grep [OPTION]... PATTERN [FILE]...
Try 'grep --help' for more information.
Piece of my code that is not working.
for i in 1;
do
case $i in
1)
projectfolder= `ls -l| grep "1*"` ;; #trying to identify individual folders 101-121
esac
done
does not localize the folders very well.
projectfolder= `ls -l| grep "1*"`
is a terrible thing to do. First, you probably intended to write projectfolder=$(ls -l| grep "1*") (using $() for readability, but the important detail is the lack of space after the =), but doing that is also a bad idea. Why not just do for i in 1*; do ...?
If your project folders all follow the naming pattern you describe, you should use brace expansion to expand to the numbers 101..121 and then easily iterate over them:
for projectfolder in {101..121} ; do
[ -d "$projectfolder" ] && echo "'${projectfolder}' exists and is a directory."
done
Brace expansion does not check for any of the directories' existence, so to see which one are actually there, you would test each one using [ -d.
Search for Brace Expansion in the bash(1) manual page and type help test for more information

Getting the most recent filename where the extension name is case *in*sensitive

I am trying to get the most recent .CSV or .csv file name among other comma separated value files where the extension name is case insensitive.
I am achieving this with the following command, provided by someone else without any explanation:
ls -t ~(i:*.CSV) | head -1
or
ls -t -- ~(i:*.CSV) | head -1
I have two questions:
What is the use of ~ and -- in this case? Does -- helps here?
How can I get a blank response when there is no .csv or .CSV file in
the folder? At the moment I get:
/bin/ls: cannot access ~(i:*.CSV): No such file or directory
I know I can test the exit code of the last command, but I was wondering maybe there is a --silent option or something.
Many thanks for your time.
PS: I made my research online quite thorough and I was unable to find an answer.
The ~ is just a literal character; the intent would appear to be to match filenames starting with ~ and ending with .csv, with i: being a flag to make the match case-insensitive. However, I don't know of any shell that supports that particular syntax. The closest thing I am aware of would be zsh's globbing flags:
setopt extended_glob # Allow globbing flags
ls ~(#i)*.csv
Here, (#i) indicates that anything after it should be matched without regard to case.
Update: as #baptistemm points out, ~(i:...) is syntax defined by ksh.
The -- is a conventional argument, supported by many commands, to mean that any arguments that follow are not options, but should be treated literally. For example, ls -l would mean ls should use the -l option to modify its output, while ls -- -l means ls should try to list a file named -l.
~(i:*.CSV) is to tell to shell (this is only supported apparently in ksh93) the enclosed text after : must be treated as insensitive, so in this example that could all these possibilites.
*.csv or
*.Csv or
*.cSv or
*.csV or
*.CSv or
*.CSV
Note this could have been written ls -t *.[CcSsVv] in bash.
To silent errors I suggest you to look for in this site for "standard error /dev/null" that will help.
I tried running commands like what you have in both bash and zsh and neither worked, so I can't help you out with that, but if you want to discard the error, you can add 2>/dev/null to the end of the ls command, so your command would look like the following:
ls -t ~(i:*.CSV) 2>/dev/null | head -1
This will redirect anything written to STDERR to /dev/null (i.e. throw it out), which, in your case, would be /bin/ls: cannot access ~(i:*.CSV): No such file or directory.

Linux command line, reverse polish notation

ls /tmp
How can I run the same command but using reverse polish notation?
Is there a mode that would allow me to do this or something similar to that?
I could use xargs but that's a lot more typing:
echo /tmp | xargs ls
This would be ideal:
/tmp ls
or
/tmp | ls
Bash (I assume you are using it) is a shell for unixoid systems.
As far as I know, bash doesn't provide such a mode. You could use a different shell that provides this feature. Searching in the web, this was my first result: https://github.com/iconmaster5326/RPOS, but maybe it is far from stable ;)
Alternatively, you can make a command that reverses it's argument list and execute it.
The usage would be like this:
reversex /tmp ls
reversex A.txt B.txt cp
Here is an example of such a command:
#!/bin/bash
for i in "$#"
do
CMDLINE="$i $CMDLINE"
done
$CMDLINE
If you name it /usr/local/bin/reversex and make it executable, you should be able to use simple reverse commands with the prefix reversex. I can not give a warranty that it works. Note that the arguments are parsed twice and have to be escaped twice, too.

how to pass asterisk into ls command inside bash script

Hi… Need a little help here…
I tried to emulate the DOS' dir command in Linux using bash script. Basically it's just a wrapped ls command with some parameters plus summary info. Here's the script:
#!/bin/bash
# default to current folder
if [ -z "$1" ]; then var=.;
else var="$1"; fi
# check file existence
if [ -a "$var" ]; then
# list contents with color, folder first
CMD="ls -lgG $var --color --group-directories-first"; $CMD;
# sum all files size
size=$(ls -lgGp "$var" | grep -v / | awk '{ sum += $3 }; END { print sum }')
if [ "$size" == "" ]; then size="0"; fi
# create summary
if [ -d "$var" ]; then
folder=$(find $var/* -maxdepth 0 -type d | wc -l)
file=$(find $var/* -maxdepth 0 -type f | wc -l)
echo "Found: $folder folders "
echo " $file files $size bytes"
fi
# error message
else
echo "dir: Error \"$var\": No such file or directory"
fi
The problem is when the argument contains an asterisk (*), the ls within the script acts differently compare to the direct ls command given at the prompt. Instead of return the whole files list, the script only returns the first file. See the video below to see the comparation in action. I don't know why it behaves like that.
Anyone knows how to fix it? Thank you.
Video: problem in action
UPDATE:
The problem has been solved. Thank you all for the answers. Now my script works as expected. See the video here: http://i.giphy.com/3o8dp1YLz4fIyCbOAU.gif
The asterisk * is expanded by the shell when it parses the command line. In other words, your script doesn't get a parameter containing an asterisk, it gets a list of files as arguments. Your script only works with $1, the first argument. It should work with "$#" instead.
This is because when you retrieve $1 you assume the shell does NOT expand *.
In fact, when * (or other glob) matches, it is expanded, and broken into segments by $IFS, and then passed as $1, $2, etc.
You're lucky if you simply retrieved the first file. When your first file's path contains spaces, you'll get an error because you only get the first segment before the space.
Seriously, read this and especially this. Really.
And please don't do things like
CMD=whatever you get from user input; $CMD;
You are begging for trouble. Don't execute arbitrary string from the user.
Both above answers already answered your question. So, i'm going a bit more verbose.
In your terminal is running the bash interpreter (probably). This is the program which parses your input line(s) and doing "things" based on your input.
When you enter some line the bash start doing the following workflow:
parsing and lexical analysis
expansion
brace expansion
tidle expansion
variable expansion
artithmetic and other substitutions
command substitution
word splitting
filename generation (globbing)
removing quotes
Only after all above the bash
will execute some external commands, like ls or dir.sh... etc.,
or will do so some "internal" actions for the known keywords and builtins like echo, for, if etc...
As you can see, the second last is the filename generation (globbing). So, in your case - if the test* matches some files, your bash expands the willcard characters (aka does the globbing).
So,
when you enter dir.sh test*,
and the test* matches some files
the bash does the expansion first
and after will execute the command dir.sh with already expanded filenames
e.g. the script get executed (in your case) as: dir.sh test.pas test.swift
BTW, it acts exactly with the same way for your ls example:
the bash expands the ls test* to ls test.pas test.swift
then executes the ls with the above two arguments
and the ls will print the result for the got two arguments.
with other words, the ls don't even see the test* argument - if it is possible - the bash expands the wilcard characters. (* and ?).
Now back to your script: add after the shebang the following line:
echo "the $0 got this arguments: $#"
and you will immediatelly see, the real argumemts how your script got executed.
also, in such cases is a good practice trying to execute the script in debug-mode, e.g.
bash -x dir.sh test*
and you will see, what the script does exactly.
Also, you can do the same for your current interpreter, e.g. just enter into the terminal
set -x
and try run the dir.sh test* = and you will see, how the bash will execute the dir.sh command. (to stop the debug mode, just enter set +x)
Everbody is giving you valuable advice which you should definitely should follow!
But here is the real answer to your question.
To pass unexpanded arguments to any executable you need to single quote them:
./your_script '*'
The best solution I have is to use the eval command, in this way:
#!/bin/bash
cmd="some command \"with_quetes_and_asterisk_in_it*\""
echo "$cmd"
eval $cmd
The eval command takes its arguments and evaluates them into the command as the shell does.
This solves my problem when I need to call a command with asterisk '*' in it from a script.

Resources