Bash history enumerated from the end - bash

In bash one can !-1 to execute the 1 command from the history enumerated from the end starting at 0. But what how can one view history so that mentioned enumeration is shown instead of usual one by built-in means? Of course one can write a simple script for that but I feel like there should be some kind of option for that.
Another question is if there is a way to somehow switch history expansion so that - sign would be unnessasary. Say exchanging meaning of !1 and !-1.

Showing negative indices is simple to implement. Just take the length of history (or get it from history 1), and subtract it from the index of all other history items.
neghistory() {
local i n s
read n s < <(history 1)
history "$#" | while read i s; do
printf '%5d %s\n' $((i-n-1)) "$s"
done
}
I don't see any built-in ways to affect history's output like this nor change how indices in history expansion works.

Related

How can i save in a list/array first parts of a line and then sort them depending on the second part?

I have a school project that gives me several lines of string in a text like this:
team1-team2:2-1
team3-team1:2-2
etc
it wants me to determine what team won (or drew) and then make a league table with them, awarding points for wins/draws.
this is my first time using bash. what i did was save team1/team2 names in a variable and then do the same for goals. how should i make the table? i managed to make my script create a new file that saves in there all team names (And checking for no duplicates) but i dont know how to continue. should i make an array for each team saving in there their results? and then how do i implement the rankings, for example
team1 3p
team2 1p
etc.
im not asking for actual code, just a guide as to how i should implement it. is making a new file the right move? should i try making a new array with the teams instead? or something else?
The problem can be divided into 3 parts:
Read the input data into memory in a format that can be manipulated easily.
Manipulate the data in memory
Output the results in the desired format.
When reading the data into memory, you might decide to read all the data in one go before manipulating it. Or you might decide to read the input data one line at a time and manipulate each line as it is read. When using shell scripting languages, like bash, the second option usually results in simpler code.
The most important decision to make here is how you want to structure the data in memory. You normally want to avoid duplication of data, and you usually want a data structure that is easy to transform into your desired output. In this case, the most logical data structure is an associative array, using the team name as the key.
Assuming that you have to use bash, here is a framework for you to build upon:
#!/bin/bash
declare -A results
while IFS=':-' read team1 team2 score1 score2; do
if [ ${score1} -gt ${score2} ]; then
((results[${team1}]+=2))
elif [ ...next test... ]; then
...
else
...
fi
done < scores.txt
# Now you have an associative array containing the points for each team.
# You can either output it as it stands, or sort it by piping through the
# 'sort' command.
for key in $[!results[#]}; do
echo ...
done
I would use awk for this
AWK is an interpreted programming language(AWK stands for Aho, Weinberger, Kernighan) designed for text processing and typically used as a data extraction and reporting tool. AWK is used largely with Unix systems.
Using pure bash scripting is often messy for that kind of jobs.
Let me show you how easy it can be using awk
Input file : scores.txt
team1-team2:2-1
team3-team1:2-2
Code :
awk -F'[:-]' ' # set delimiters to ':' or '-'
{
if($3>$4){teams[$1] += 3} # first team gets 3 points
else if ($3<$4){teams[$2] += 3} # second team gets 3 points
else {teams[$1]+=1; teams[$2]+=1} # both teams get 1 point
}
END{ # after scanning input file
for(team in teams){
print(team OFS teams[team]) # print total points per team
}
}' scores.txt | sort -rnk 2 > ranking.txt # sort by nb of points
Output (ranking.txt):
team1 4
team3 1

Checking if a word is already in a list (Bash)

I'm writing a small bash script and am trying to test is a newly generated word is already in a list of all previously made words.
This is what I'm working with now:
dict=("word1"... "word21") #there would be 21 words in here
prev_guesses=()
guess_creator() {
guess=""
for i in {1..5} ;
do
guess_num=$( shuf -i 0-21 -n 1 )
guess+="${dict[$guess_num]}"
done
# using recursion to take another guess
if [ $guess (is in) $prev_guesses] ; then
guess_creator
else
prev_guess+=($guess)
fi
}
I'm also not sure if recursion works like this in bash. If it doesn't, I'm asking here how to actually "unbreak" this code.The idea is to have this function constantly outputting a unique string every time it runs so I can use it later on in the script.
I have three questions:
How can I compare guess to the list prev_guesses and get a true or false output
How can I append guessed string to the list prev_guesses (I just checked it and it is just concatenating the strings together, I need a list like prev_guesses=("guess1" "guess2"...) - I may have solved this with the final edit.
Does this recursion in guess_creator work?
Associative Arrays
Since you are only interested in »is this word in the list or not?« but not in the order of entries, you could use an associative array (also known as dictionary or hash map) to store your words. Checking whether an entry is in such a map is very fast (time complexity O(1)):
declare -A oldGuesses=([word1]= [word2]= [word3]=)
if [[ "${oldGuesses[$guess]+1}" ]]; then
echo "$guess was already taken"
else
echo "$guess was not taken yet"
fi
You can add an entry to dict using
dict["newEntry"]=
Don't worry about the empty right hand side. Maps are normally used to store key-value pairs. Here we only use the keys (the things which are written inside the []).
Avoiding the list of guesses completely
You mentioned that you want to bruteforce and that the list could grow up to 4M entries. I would advise against using bash, but even more against storing all guesses at all (no matter what language you are using).
Instead, enumerate all possible guesses in an ordered way:
You want to create guesses which are five concatenated words?
Just create five for-loops:
for w1 in "${dict[#]}"; do
for w2 in "${dict[#]}"; do
for w3 in "${dict[#]}"; do
for w4 in "${dict[#]}"; do
for w5 in "${dict[#]}"; do
guess="$w1$w2$w3$w4$w5"
# do something with your guess here
done
done
done
done
done
Benefits of this approach over your old approach:
Don't have to store 4M guesses.
Don't have to search through 4M guesses whenever taking a new guess.
Guarantees that the same guess is not picked over and over again.
Terminates when all possible guesses are made.
There's nothing like that in bash for arrays (Socowi's idea of using Associative Array is better), you would have to iterate through the list again, or maybe try to use grep or something
to refer to all the elements of an array you need the syntax ${prev_guesses[*]}
so you can concatenate with something like
prev_guesses=(${prev_guesses[*]} $guess)
Spaces in your words would make it all more complicated
It should do. BUT....
That's the hard way. If you want to avoid repeating guesses, better to take out each guess from the array when you take it, so you can't take it again.
Easier still is to use the shuf commmand to do everything
guess=($( shuf -e ${dict[*]} -n 5))
shuffle your words and take the first five

How can I create a list from a single url and append a numerical value to the end of every line

I need to provide a listing of a website's pages. The only thing to change per line is the page number at the end of the line. So for example, I need to take:
mywebsite.com/things/stuff/?q=content&page=1
And from that generate a sequential listing of pages:
mywebsite.com/things/stuff/?q=content&page=1
mywebsite.com/things/stuff/?q=content&page=2
mywebsite.com/things/stuff/?q=content&page=3
I need to list all pages between 1 - 120.
I have been using bash but any shell that gets the job done is fine. I don't have any code to show because I simply just don't know how to begin. It sounds simple enough but so far I'm completely at a loss as to how I can accomplish this.
With GNU bash 4:
printf '%s\n' 'mywebsite.com/things/stuff/?q=content&page='{1..120}
You can simply use:
for i in $(seq 120); do echo 'mywebsite.com/things/stuff/?q=content&page='"$i"; done > list.txt

storage in awk when used within shell script

I am writing a shell script program in which I am internally calling an awk script. Here is my script below.
for FILE in `eval echo{0..$fileIterator}`
{
if(FILE == $fileIterator)
{
printindicator =1;
}
grep RECORD FILEARRAY[FILE]| awk 'for(i=1;i<=NF;i++) {if($i ~ XXXX) {XARRAY[$i]++}} END {if(printIndicator==1){for(element in XARRAY){print element >> FILE B}}'
I hope I am clear with my code . Please let me know if you need any other details.
ISSUE
My motivation in this program is to traverse through all the files an get the lines that has "XXXX" in all the files and store the lines in an array. That is what I am doing here. Finally I need to store the contents of the array variable into a file. I can store the contents at each and every step like the below
{if($i ~ XXXX) {XARRAY[$i]++; print XARRAY[$i] >> FILE B}}
But the reason behind not going to this approach is each time I need to do an I/O operation and for this the time taken is much and that is why I am converting that into inmemory everytime and then at last dumping the in memory array(XARRAY) into the file.
The problem I am facing here is that. The shell script calls the awk everytime, the data's are getting stored in the array(XARRAY) but for the next iteration, the previous content of XARRAY is getting deleted and it puts the new content as this assumes this as a new array. Hence at last when I print the contents, it prints only the lately updated XARRAY and not all the data that is expected from this.
SUGGESTIONS EXPECTED
1) How to make the awk script realize that the XARRAY is an old one and not the new one when it is being called everytime in each iteration.
2) One of the alternative is to do an I/O everytime. But I am not interested in this. Is there any other alternative other than this. Thank you.
This post involves combining shell script and awk script to solve a problem. This is very often a useful approach, as it can leverage the strengths of each, and potentially keep the code from getting ugly in either!
You can indeed "preserve state" with awk, with a simple trick: leveraging a coprocess from the shell script (bash, ksh, etc. support coprocess).
Such a shell script launches one instance of awk as a coprocess. This awk instance runs your awk code, which continuously processes its lines of input, and accumulates stateful information as desired.
The shell script continues on, gathering up data as needed, and passes data to the awk coprocess whenever ready. This can run in a loop, potentially blocking or sleeping, potentially acting as a long-running background daemon. Highly versatile!
In your awk script, you need a strategy for triggering the output of the stateful data it has been accumulating. The simplest, is to have an END{} action which triggers when awk stdin closes. If you need output data sooner than that, at each line of input the awk code has a chance to output its data.
I have successfully used this approach many times.
Ouch, can't tell if it is meant to be real or pseudocode!
You can't make awk preserve state. You would either have to save it to a temporary file or store it in a shell variable, the contents of which you'd pass to later invocations. But this is all too much hassle for what I understand you want to achieve.
I suggest you omit the loop, which will allow you to call awk only once with just some reordering. I assume FILE A is the FILE in the loop and FILE B is something external. The reordering would end up something very roughly like:
grep RECORD ${FILEARRAY[#]:0:$fileIterator} | awk 'for(i=1;i<=NF;i++) {if($i ~ XXXX) {XARRAY[$i]++}} END {for(element in XARRAY){print element >> FILEB}'
I move the filename expansion to the grep call and removed the whole printIndicator check.
It could all be done even more efficiently (the obvious one being removal of grep), but you provided too little detail to make early optimisation sensible.
EDIT: fixed the loop iteration with the info from the update. Here's a loopy solution, which is immune to new whitespace issues and too long command lines:
for FILE in $(seq 0 $fileIterator); do
grep RECORD "${FILEARRAY[$FILE]}"
done |
awk 'for(i=1;i<=NF;i++) {if($i ~ XXXX) {XARRAY[$i]++}} END {for(element in XARRAY){print element >> FILEB}'
It still runs awk only once, constantly feeding it data from the loop.
If you want to load the results into an array UGUGU, do the following as well (requires bash 4):
mapfile UGUGU < FILEB
results=$(for loop | awk{for(element in XARRAY)print element})..
I declared result as an array so for every "element" that is being printed it should store in results[1], results[2].
But instead of this, it is performing the below ...
Lets assume
element = "I am fine"(First iteration of for loop),
element = "How are you" (Second iteration of for loop).
My expected result in accordance to this is,
results[1]= "I am fine" and results[2] = "How are you" ,
but the output I am getting is results[1]= "I" results[2]= "am". I dont know why it is delimiting by space .. Any suggestions regarding this

Automatic update data file and display?

I am trying to develop a system where this application allows user to book ticket seat. I am trying to implement an automatic system(a function) where the app can choose the best seats for the user.
My current database(seats.txt) file store in this way(not sure if its a good format:
X000X
00000
0XXX0
where X means the seat is occupied, 0 means nothing.
After user login to my system, and choose the "Choose best for you", the user will be prompt to enter how many seats he/she want (I have done this part), now, if user enter: 2, I will check from first row, see if there is any empty seats, if yes, then I assign(this is a simple way, once I get this work, I will write a better "automatic-booking" algorithm)
I try to play with sed, awk, grep.. but it just cant work (I am new to bash programming, just learning bash 3 days ago).
Anyone can help?
FYI: The seats.txt format doesn't have to be that way. It can also be, store all seats in 1 row, like: X000X0XXX00XXX
Thanks =)
Here's something to get you started. This script reads in each seat from your file and displays if it's taken or empty, keeping track of the row and column number all the while.
#!/bin/bash
let ROW=1
let COL=1
# Read one character at a time into the variable $SEAT.
while read -n 1 SEAT; do
# Check if $SEAT is an X, 0, or other.
case "$SEAT" in
# Taken.
X) echo "Row $ROW, col $COL is taken"
let COL++
;;
# Empty.
0)
echo "Row $ROW, col $COL is EMPTY"
let COL++
;;
# Must be a new line ('\n').
*) let ROW++
let COL=1
;;
esac
done < seats.txt
Notice that we feed in seats.txt at the end of the script, not at the beginning. It's weird, but that's UNIX for ya. Curiously, the entire while loop behaves like one big command:
while read -n 1 SEAT; do {stuff}; done < seats.txt
The < at the end feeds in seats.txt to the loop as a whole, and specifically to the read command.
It's not really clear what help you're asking for here. "Anyone can help?" is a very broad question.
If you're asking if you're using the right tools then yes, the text processing tools (sed/awk/grep et al) are ideal for this given the initial requirement that it be done in bash in the first place. I'd personally choose a different baseline than bash but, if that's what you've decided, then your tool selection is okay.
I should mention that bash itself can do a lot of the things you'll probably be doing with the text processing tools and without the expense of starting up external processes. But, since you're using bash, I'm going to assume that performance is not your primary concern (don't get me wrong, bash will probably be fast enough for your purposes).
I would probably stick with the multi-line data representation for two reasons. The first is that simple text searches for two seats together will be easier if you keep the rows separate from each other. Otherwise, in the 5seat-by-2row XXXX00XXXX, a simplistic search would consider those two 0 seats together despite the fact they're nowhere near each other:
XXXX0
0XXXX
Secondly, some people consider the row to be very important. I won't sit in the first five rows at the local cinema simply because I have to keep moving my head to see all the action.
By way of example, you can get the front-most row with two consecutive seats with (commands are split for readability):
pax> cat seats.txt
X000X
00000
0XXX0
pax> expr $(
(echo '00000';cat seats.txt)
| grep -n 00
| tail -1
| sed 's/:.*//'
) - 1
2
The expr magic and extra echo are to ensure you get back 0 if no seats are available. And you can get the first position in that row with:
pax> cat seats.txt
| grep 00
| tail -1
| awk '{print index($0,"00")}'
3

Resources