shell script to find all files in /etc with at least 7 hard links - shell

Using standard UNIX tools (grep, awk, shell builtins, etc), I need to output any file that has at least 7 hard links in the /etc directory.
Any help with this would be appreciated.

Unfortunately, find doesn't have a predicate for this, so you end up needing to do your own filtering. Assuming you have the GNU version of find, though, it can output link count, even if it can't filter by it on its own:
#!/usr/bin/env bash
# ^^^^- NOT /bin/sh; we need the ability to tell read to stop on a NUL.
while IFS= read -r -d ' ' link_count && IFS= read -r -d '' filename; do
(( link_count >= 7 )) && printf '%q\n' "$filename"
done < <(find /etc -printf '%n %p\0')
To answer some likely questions regarding the above:
What's with the -printf '%n %p\0'?
%n prints hardlink count.
%P prints the name of the file that was found.
\0 prints a NUL character -- the only character that's guaranteed not to be part of a filename, and which is thus safe to separate them with (filenames can have newlines inside of them, so newline-separated lists of names can be misleading!)
What's with the (( link_count >= 7 )) syntax? - See arithmetic expression on the bash-hackers' wiki.
What's with the printf '%q\n' "$filename"? - It prints names in a human-readable way that will escape any characters that aren't printable (tabs, whitespaces, newlines, etc) -- and which you can copy-and-paste into a bash prompt to refer to the same file.
Why a while read loop? - See BashFAQ #1.
Why read -d ' ' and then read -d ''? - The -d argument to read tells it to stop when it sees the first character of the succeeding argument. read -d ' ' tells it to stop when it sees a space; read -d '' tells it to stop when it sees a NUL (as a 0-length C string has one character in it -- the NUL terminator).

I think the question is ill-posed, and presents the misconception that there are any files at all in /etc. /etc is a directory, and as such it does not contain any files. It contains only names, which are references to files. Perhaps a short answer to the question is as simple as:
ls -ila /etc | awk '$3 > 7'
, which will list any of the names in /etc that link to the file with 7 or more links, but there is certainly no guarantee that all of those links are themselves in /etc. I suspect the question is intended to be worded as "list any file that has a link in /etc that has at least 7 total links", in which case I would give the answer as:
for i in /etc/*; do stat -c '%h %i' "$i"; done |
awk '$1 > 7{a[$2]++} END {for (node in a) print node}'
Or, if you just want to list all of links that are in /etc, do:
for i in /etc/*; do stat -c '%h %n' "$i"; done | awk '$1 > 7'
Use find If you want to do it recursively.

Related

Batch Renaming files to a sequence [duplicate]

I want to rename the files in a directory to sequential numbers. Based on creation date of the files.
For Example sadf.jpg to 0001.jpg, wrjr3.jpg to 0002.jpg and so on, the number of leading zeroes depending on the total amount of files (no need for extra zeroes if not needed).
Beauty in one line:
ls -v | cat -n | while read n f; do mv -n "$f" "$n.ext"; done
You can change .ext with .png, .jpg, etc.
Try to use a loop, let, and printf for the padding:
a=1
for i in *.jpg; do
new=$(printf "%04d.jpg" "$a") #04 pad to length of 4
mv -i -- "$i" "$new"
let a=a+1
done
using the -i flag prevents automatically overwriting existing files, and using -- prevents mv from interpreting filenames with dashes as options.
I like gauteh's solution for its simplicity, but it has an important drawback. When running on thousands of files, you can get "argument list too long" message (more on this), and second, the script can get really slow. In my case, running it on roughly 36.000 files, script moved approx. one item per second! I'm not really sure why this happens, but the rule I got from colleagues was "find is your friend".
find -name '*.jpg' | # find jpegs
gawk 'BEGIN{ a=1 }{ printf "mv %s %04d.jpg\n", $0, a++ }' | # build mv command
bash # run that command
To count items and build command, gawk was used. Note the main difference, though. By default find searches for files in current directory and its subdirectories, so be sure to limit the search on current directory only, if necessary (use man find to see how).
A very simple bash one liner that keeps the original extensions, adds leading zeros, and also works in OSX:
num=0; for i in *; do mv "$i" "$(printf '%04d' $num).${i#*.}"; ((num++)); done
Simplified version of http://ubuntuforums.org/showthread.php?t=1355021
using Pero's solution on OSX required some modification. I used:
find . -name '*.jpg' \
| awk 'BEGIN{ a=0 }{ printf "mv \"%s\" %04d.jpg\n", $0, a++ }' \
| bash
note: the backslashes are there for line continuation
edit July 20, 2015:
incorporated #klaustopher's feedback to quote the \"%s\" argument of the mv command in order to support filenames with spaces.
with "rename" command
rename -N 0001 -X 's/.*/$N/' *.jpg
or
rename -N 0001 's/.*/$N.jpg/' *.jpg
To work in all situations, put a \" for files that have space in the name
find . -name '*.jpg' | gawk 'BEGIN{ a=1 }{ printf "mv \"%s\" %04d.jpg\n", $0, a++ }' | bash
On OSX, install the rename script from Homebrew:
brew install rename
Then you can do it really ridiculously easily:
rename -e 's/.*/$N.jpg/' *.jpg
Or to add a nice prefix:
rename -e 's/.*/photo-$N.jpg/' *.jpg
NOTE The rename commands here include -n which previews the rename. To actually perform the renaming, remove the -n
If your rename doesn't support -N, you can do something like this:
ls -1 --color=never -c | xargs rename -n 's/.*/our $i; sprintf("%04d.jpg", $i++)/e'
NOTE The rename commands here includes -n which previews the rename. To actually perform the renaming, remove the -n
Edit To start with a given number, you can use the (somewhat ugly-looking) code below, just replace 123 with the number you want:
ls -1 --color=never -c | xargs rename -n 's/.*/our $i; if(!$i) { $i=123; } sprintf("%04d.jpg", $i++)/e'
This lists files in order by creation time (newest first, add -r to ls to reverse sort), then sends this list of files to rename. Rename uses perl code in the regex to format and increment counter.
However, if you're dealing with JPEG images with EXIF information, I'd recommend exiftool
This is from the exiftool documentation, under "Renaming Examples"
exiftool '-FileName<CreateDate' -d %Y%m%d_%H%M%S%%-c.%%e dir
Rename all images in "dir" according to the "CreateDate" date and time, adding a copy number with leading '-' if the file already exists ("%-c"), and
preserving the original file extension (%e). Note the extra '%' necessary to escape the filename codes (%c and %e) in the date format string.
Follow command rename all files to sequence and also lowercase extension:
rename --counter-format 000001 --lower-case --keep-extension --expr='$_ = "$N" if #EXT' *
find . | grep 'avi' | nl -nrz -w3 -v1 | while read n f; do mv "$f" "$n.avi"; done
find . will display all file in folder and subfolders.
grep 'avi' will filter all files with avi extension.
nl -nrz -w3 -v1 will display sequence number starting 001 002 etc following by file name.
while read n f; do mv "$f" "$n.avi"; done will change file name to sequence numbers.
I spent 3-4 hours developing this solution for an article on this:
https://www.cloudsavvyit.com/8254/how-to-bulk-rename-files-to-numeric-file-names-in-linux/
if [ ! -r _e -a ! -r _c ]; then echo 'pdf' > _e; echo 1 > _c ;find . -name "*.$(cat _e)" -print0 | xargs -0 -t -I{} bash -c 'mv -n "{}" $(cat _c).$(cat _e);echo $[ $(cat _c) + 1 ] > _c'; rm -f _e _c; fi
This works for any type of filename (spaces, special chars) by using correct \0 escaping by both find and xargs, and you can set a start file naming offset by increasing echo 1 to any other number if you like.
Set extension at start (pdf in example here). It will also not overwrite any existing files.
Let us assume we have these files in a directory, listed in order of creation, the first being the oldest:
a.jpg
b.JPG
c.jpeg
d.tar.gz
e
then ls -1cr outputs exactly the list above. You can then use rename:
ls -1cr | xargs rename -n 's/^[^\.]*(\..*)?$/our $i; sprintf("%03d$1", $i++)/e'
which outputs
rename(a.jpg, 000.jpg)
rename(b.JPG, 001.JPG)
rename(c.jpeg, 002.jpeg)
rename(d.tar.gz, 003.tar.gz)
Use of uninitialized value $1 in concatenation (.) or string at (eval 4) line 1.
rename(e, 004)
The warning ”use of uninitialized value […]” is displayed for files without an extension; you can ignore it.
Remove -n from the rename command to actually apply the renaming.
This answer is inspired by Luke’s answer of April 2014. It ignores Gnutt’s requirement of setting the number of leading zeroes depending on the total amount of files.
I had a similar issue and wrote a shell script for that reason. I've decided to post it regardless that many good answers were already posted because I think it can be helpful for someone. Feel free to improve it!
numerate
#Gnutt The behavior you want can be achieved by typing the following:
./numerate.sh -d <path to directory> -o modtime -L 4 -b <startnumber> -r
If the option -r is left out the reaming will be only simulated (Should be helpful for testing).
The otion L describes the length of the target number (which will be filled with leading zeros)
it is also possible to add a prefix/suffix with the options -p <prefix> -s <suffix>.
In case somebody wants the files to be sorted numerically before they get numbered, just remove the -o modtime option.
a=1
for i in *.jpg; do
mv -- "$i" "$a.jpg"
a=`expr $a + 1`
done
Again using Pero's solution with little modifying, because find will be traversing the directory tree in the order items are stored within the directory entries. This will (mostly) be consistent from run to run, on the same machine and will essentially be "file/directory creation order" if there have been no deletes.
However, in some case you need to get some logical order, say, by name, which is used in this example.
find -name '*.jpg' | sort -n | # find jpegs
gawk 'BEGIN{ a=1 }{ printf "mv %s %04d.jpg\n", $0, a++ }' | # build mv command
bash # run that command
The majority of the other solutions will overwrite existing files already named as a number. This is particularly a problem if running the script, adding more files, and then running the script again.
This script renames existing numerical files first:
#!/usr/bin/perl
use strict;
use warnings;
use File::Temp qw/tempfile/;
my $dir = $ARGV[0]
or die "Please specify directory as first argument";
opendir(my $dh, $dir) or die "can't opendir $dir: $!";
# First rename any files that are already numeric
while (my #files = grep { /^[0-9]+(\..*)?$/ } readdir($dh))
{
for my $old (#files) {
my $ext = $old =~ /(\.[^.]+)$/ ? $1 : '';
my ($fh, $new) = tempfile(DIR => $dir, SUFFIX => $ext);
close $fh;
rename "$dir/$old", $new;
}
}
rewinddir $dh;
my $i;
while (my $file = readdir($dh))
{
next if $file =~ /\A\.\.?\z/;
my $ext = $file =~ /(\.[^.]+)$/ ? $1 : '';
rename "$dir/$file", sprintf("%s/%04d%s", $dir, ++$i, $ext);
}
Sorted by time, limited to jpg, leading zeroes and a basename (in case you likely want one):
ls -t *.jpg | cat -n | \
while read n f; do mv "$f" "$(printf thumb_%04d.jpg $n)"; done
(all on one line, without the \)
Not related to creation date but numbered based on sorted names:
python3 -c \
'ext="jpg"
start_num=0
pad=4
import os,glob
files=glob.glob(f"*.{ext}")
files.sort()
renames=list(zip(files,range(start_num,len(files)+start_num)))
for r in renames:
oname=r[0]
nname=f"{r[1]:0{pad}}.{ext}"
print(oname,"->",nname)
os.rename(oname,nname)
'
This script will sort the files by creation date on Mac OS bash. I use it to mass rename videos. Just change the extension and the first part of the name.
ls -trU *.mp4| awk 'BEGIN{ a=0 }{ printf "mv %s lecture_%03d.mp4\n", $0, a++ }' | bash
ls -1tr | rename -vn 's/.*/our $i;if(!$i){$i=1;} sprintf("%04d.jpg", $i++)/e'
rename -vn - remove n for off test mode
{$i=1;} - control start number
"%04d.jpg" - control count zero 04 and set output extension .jpg
To me this combination of answers worked perfectly:
ls -v | gawk 'BEGIN{ a=1 }{ printf "mv %s %04d.jpg\n", $0, a++ }' | bash
ls -v helps with ordering 1 10 9 in correct: 1 9 10 order, avoiding filename extension problems with jpg JPG jpeg
gawk 'BEGIN{ a=1 }{ printf "mv %s %04d.jpg\n", $0, a++ }' renumbers with 4 characters and leading zeros. By avoiding mv I do not accidentally try to overwrite anything that is there already by accidentally having the same number.
bash executes
Be aware of what #xhienne said, piping unknown content to bash is a security risk. But this was not the case for me as I was using my scanned photos.
Here is what worked for me.
I Have used rename command so that if any file contains spaces in name of it then , mv command dont get confused between spaces and actual file.
Here i replaced spaces , ' ' in a file name with '_' for all jpg files
#! /bin/bash
rename 'y/ /_/' *jpg #replacing spaces with _
let x=0;
for i in *.jpg;do
let x=(x+1)
mv $i $x.jpg
done
Nowadays there is an option after you select multiple files for renaming (I have seen in thunar file manager).
select multiple files
check options
select rename
A prompt comes with all files in that particular dir
just check with the category section
Using sed :
ls -tr | sed "s/(.*)/mv '\1' \=printf('%04s',line('.').jpg)/" > rename.sh
bash rename.sh
This way you can check the script before executing it to avoid big mistakes
Here a another solution with "rename" command:
find -name 'access.log.*.gz' | sort -Vr | rename 's/(\d+)/$1+1/ge'
Pero's answer got me here :)
I wanted to rename files relative to time as the image viewers did not display images in time order.
ls -tr *.jpg | # list jpegs relative to time
gawk 'BEGIN{ a=1 }{ printf "mv %s %04d.jpg\n", $0, a++ }' | # build mv command
bash # run that command
To renumber 6000, files in one folder you could use the 'Rename' option of the ACDsee program.
For defining a prefix use this format: ####"*"
Then set the start number and press Rename and the program will rename all 6000 files with sequential numbers.

Get index of argument with xargs?

In bash, I have list of files all named the same (in different sub directories) and I want to order them by creation/modified time, something like this:
ls -1t /tmp/tmp-*/my-file.txt | xargs ...
I would like to rename those files with some sort of index or something so I can move them all into the same folder. My result would ideally be something like:
my-file0.txt
my-file1.txt
my-file2.txt
Something like that. How would I go about doing this?
You can just loop through these files and keep appending an incrementing counter to desired file name:
for f in /tmp/tmp-*/my-file.txt; do
fname="${f##*/}"
fname="${fname%.*}"$((i++)).txt
mv "$f" "/dest/dir/$fname"
done
EDIT: In order to sort listed files my modification time as is the case with ls -1t you can use this script:
while IFS= read -d '' -r f; do
f="${f#* }"
fname="${f##*/}"
fname="${fname%.*}"$((i++)).txt
mv "$f" "/dest/dir/$fname"
done < <(find /tmp/tmp-* -name 'my-file.txt' -printf "%T# %p\0" | sort -zk1nr)
This handles filenames with all special characters like white spaces, newlines, glob characters etc since we are ending each filename with NUL or \0 character in -printf option. Note that we are also using sort -z to handle NUL terminated data.
So I found an answer to my own question, thoughts on this one?
ls -1t /tmp/tmp-*/my-file.txt | awk 'BEGIN{ a=0 }{ printf "cp %s /tmp/all-the-files/my-file_%03d.txt\n", $0, a++ }' | bash;
I found this from another stack overflow question looking for something similar that my search didn't find at first. I was impressed with the awk line, thought that was pretty neat.

How to compare latest two files are identical or not with shell?

I wanna check whether the latest two files are different or not.
This is my code, it does not work.
#!/bin/bash
set -x -e
function test() {
ls ~/Downloads/* -t | head -n 2 | xargs cmp -s
echo $?
}
test
Thanks.
Assuming that you have GNU find and GNU sort:
#!/bin/bash
# ^^^^ - not /bin/sh, which lacks <()
{
IFS= read -r -d '' file1
IFS= read -r -d '' file2
} < <(find ~/Downloads -type f -mindepth 1 -maxdepth 1 -printf '%T# %p\0' | sort -r -n -z)
if cmp -s -- "$file1" "$file2"; then
echo "Files are identical"
else
echo "Files differ"
fi
If your operating system is MacOS X and you have GNU findutils and coreutils installed through MacPorts, homebrew or fink, you might need to replace the find with gfind and the sort with gsort to get GNU rather than BSD implementations of these tools.
Key points here:
find is asked to emit a stream in the form of [epoch-timestamp] [filename][NULL]. This is done because NUL is the only character that cannot exist in a pathname.
sort is asked to sort this stream numerically.
The first two items of the stream are read into shell variables.
Using the -- argument to cmp after options and before positional arguments ensures that filenames can never be parsed as positional arguments, even if the were to start with -.
So, why not use ls -t? Consider (as an example) what happens if you have a file created with the command touch $'hello\nworld', with a literal newline partway through its name; depending on your version of ls, it may be emitted as hello?world, hello^Mworld or hello\nworld (in any of these cases, a filename that doesn't actually exist if treated as literal and not glob-expanded), or as two lines, hello, and world. This would mess up the rest of your pipeline (and things as simple as filenames with spaces will also break xargs with default options, as will filenames with literal quotes; xargs is only truly safe when used with the argument -0 to treat content as NUL-delimited, though it's less unsafe than defaults when used with the GNU extension -d $'\n').
See also:
ParsingLs ("Why you shouldn't parse the output of ls")
BashFAQ #3 ("How can I find the latest (newest, earliest, oldest) file in a directory?")

"filename too long" bash mv command old files

#! /bin/sh -
cd /PHOTAN || exit
fn=$(ls -t | tail -n -30)
mv -f -- "${fn}" /old
all I want todo is keep most recent 30 files... but cant get past the mv
"File name too long" problem
please help'
The notation "${fn}" adds all the file names into a single argument string, separated by spaces. Just for once, assuming you don't have to worry about file names with spaces in them, you need:
mv -f -- ${fn} /old
If you have file names with spaces in them, then you've got problems starting with parsing the output of the ls command.
But what if you do have to worry about spaces in your filenames?
Then, as I stated, you have major problems, starting with the issues of parsing the output of ls.
$ echo > 'a b'
$ echo > ' c d '
$
Two nice file names with spaces in them. They cause merry hell. I'm about to assume you're on Linux or something similar enough. You need to use bash arrays, the stat command, printf, sort -z, sed -z. Or you should simply outlaw filenames with spaces; it is probably easier.
names=( * )
The array names contains each file name as a separate array element, leading and trailing and embedded blanks all handled correctly.
names=( * )
for file in "${names[#]}"
do printf "%s\0" "$(stat -c '%Y' "$file") $file"
done |
sort -nzr |
sed -nze '1,30s/^[0-9][0-9]* //p' |
tr '\0' '\n'
The for loop evaluates the modification time of each file separately, and combines the modification time, a space, and the file name into a single string followed by a null byte to mark the end of the string. The sort command sorts the 'lines' numerically, assuming the lines are terminated by null bytes because of the -z option, and places the most recent file names first. The sed command prints the first 30 'lines' (file names) only; the tr command replaces null bytes with newlines (but in doing so, loses the identity of file name boundaries).
The code works even with file names containing newlines, but only on systems where sed and sort support the (non-standard) -z option to process null-terminated input 'lines' — that means systems using GNU sed and sort (even BSD sed as found on Mac OS X does not, though the Mac OS X sort is GNU sort and does support -z).
Ugh! The shell was designed for spaces to appear between and not within file names.
As noted by BroSlow in a comment, if you assume 'no newlines in filenames', then the code can be simpler and more nearly portable — but it is still tricky:
ls -t |
tail -30 |
{
list=()
while IFS='' read -r file
do list+=( "$file" )
done
mv -f -- "${list[#]}" /old
}
The IFS='' is needed so that leading and trailing spaces in filenames are preserved (and tabs, too).
I note in passing that the Korn shell would not require the braces but Bash does.

Loop through text file and execute If Then statement for each line with bash

I have a command that lists the full 8 level deep path of all folders we are backing up.
I also have a command that enumerates all 8 level deep folders on the system.
Both of these are stored as variables in a bash script.
I'm trying to get a loop together that takes file 1 and uses the first line entry as a variable in an if/then/else, and then moves onwards to through the end of the file.
I've tried so many things but its beyond my skillset to provide an example that won't confuse the reader of this post.
TempFile1=/ifs/data/scripts/ConfigMonitor/TempFile1.txt
TempFile2=/ifs/data/scripts/ConfigMonitor/TempFile2.txt
find /ifs/*/*/backup -maxdepth 4 -mindepth 4 -type d > $TempFile1
isi snapshot schedules list -v | grep Path: | awk '{print $2}' > $TempFile2
list line 1 on $TempFile1
Grep for line 1 within $TempFile2
if result yielded then
echo found
else
echo fullpath not being backed up
fi
Use Grep's -f Flag
grep(1) says:
-f FILE, --file=FILE
Obtain patterns from FILE, one per line. The empty file
contains zero patterns, and therefore matches nothing. (-f is
specified by POSIX.)
Therefore, the following should work:
grep -f patterns_to_match.txt file_to_examine.txt
Faster Reporting
Another way to think about this is that you can ask GNU grep to show you all the matches:
echo 'Lines that match a pattern in your pattern file.'
grep -f patterns_to_match.txt file_to_examine.txt
and then show you all the lines that don't match any of the patterns:
echo 'Lines that do not match any patterns in your pattern file.'
grep -f patterns_to_match.txt -v file_to_examine.txt
This is likely to be faster and more efficient than looping through the file one line at a time in Bash. You may or may not get similar results with a grep other than GNU grep; while the -f and -v flags are specified by POSIX, I only tested it against GNU grep 2.16, so your mileage may vary.
This should iterate through Tempfile1.txt and grep for the line in TempFile2.txt.
while read line; do
if grep $line /path/to/TempFile2.txt > /dev/null
then
echo "Found $line"
else
echo "Did not find $line"
fi
done < Tempfile1.txt
Tempfile1.txt:
a
b
c
Tempfile2.txt
b
d
z
Output:
Did not find a
Found b
Did not find c

Resources