23

I have a file named $dir and a string named $line, I know that this string is a complete line of that file but I don't know its line number and I want to remove it from file, what should I do?

Is it possible to use awk?

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
ibrahim
  • 3,254
  • 7
  • 42
  • 56

10 Answers10

20

Read the lines one by one, and write all but the matching line to another file. Then replace the original file.

Ignacio Vazquez-Abrams
  • 776,304
  • 153
  • 1,341
  • 1,358
20
$contents = file_get_contents($dir);
$contents = str_replace($line, '', $contents);
file_put_contents($dir, $contents);
Naveed Ahmad
  • 3,176
  • 1
  • 15
  • 18
  • i tried that code writing echo between each line, its fine until file_put_contents function, i dont understand why it doesnt work:S – ibrahim Apr 19 '11 at 08:38
  • @ibrahim - What do you mean didn't work? Could you get all the contents through `file_get_contents`. Tell me exactly which part didn't work. – Naveed Ahmad Apr 19 '11 at 08:41
  • "file_put_contents" doesn't write contents into the file – ibrahim Apr 19 '11 at 09:30
  • May be you don't have write permissions on the file. Take a look: http://php.net/manual/en/function.file-put-contents.php – Naveed Ahmad Apr 19 '11 at 09:33
  • It just find and replace some part of contents and not remove complete line as there is in above question, so this is better answer: https://stackoverflow.com/a/47733944/1407491 – Nabi K.A.Z. Dec 09 '17 at 22:39
  • 4
    To delete the line completely: `$contents = str_replace($line."\n", '', $contents);` – Andrés Chandía Jan 24 '19 at 14:57
  • @AndrésChandía by adding `\n` the `str_replace` can't find the `$line` string – Amir Mar 09 '20 at 20:48
  • 3
    @Amir Some times line brakes are made differently, I've just tasted my solution and it worked as expected. Anyway, try `$contents = str_replace($line.PHP_EOL, '', $contents);` instead – Andrés Chandía Mar 10 '20 at 11:41
12

this will just look over every line and if it not what you want to delete, it gets pushed to an array that will get written back to the file. see this

 $DELETE = "the_line_you_want_to_delete";

 $data = file("./foo.txt");

 $out = array();

 foreach($data as $line) {
     if(trim($line) != $DELETE) {
         $out[] = $line;
     }
 }

 $fp = fopen("./foo.txt", "w+");
 flock($fp, LOCK_EX);
 foreach($out as $line) {
     fwrite($fp, $line);
 }
 flock($fp, LOCK_UN);
 fclose($fp);  
Community
  • 1
  • 1
Thusitha Sumanadasa
  • 1,669
  • 2
  • 22
  • 30
  • For simplicity why not build the `$out` as a string rather than an array and then use **file_put_contents** with LOCK_EX? Any reason for this approach? – Cyrille Mar 02 '23 at 13:10
6

It can be solved without the use of awk:

function remove_line($file, $remove) {
    $lines = file($file, FILE_IGNORE_NEW_LINES);
    foreach($lines as $key => $line) {
        if($line === $remove) unset($lines[$key]);
    }
    $data = implode(PHP_EOL, $lines);
    file_put_contents($file, $data);
}
Michael
  • 2,631
  • 2
  • 24
  • 40
Nabi K.A.Z.
  • 9,887
  • 6
  • 59
  • 81
  • The SO is asking about awk. Your answer may solve the problem, but try to explain how you solve it. – Michael Dec 10 '17 at 05:41
  • 1
    @Michael, The SO is asking two question! 1)what should I do? 2)Is it possible to use awk?, so I did answer to question one. and also see the best answer marked, it didn't use `awk`. – Nabi K.A.Z. Jul 17 '18 at 17:24
5

Another approach is to read the file line by line until you find a match, then truncate the file to that point, and then append the rest of the lines.

mpen
  • 272,448
  • 266
  • 850
  • 1,236
3

This is also good if you're looking for a substring (ID) in a line and want to replace the old line with the a new line.

Code:

$contents = file_get_contents($dir);
$new_contents = "";
if (strpos($contents, $id) !== false) { // if file contains ID
    $contents_array = explode(PHP_EOL, $contents);
    foreach ($contents_array as &$record) {    // for each line
        if (strpos($record, $id) !== false) { // if we have found the correct line
            continue; // we've found the line to delete - so don't add it to the new contents.
        } else {
            $new_contents .= $record . "\r"; // not the correct line, so we keep it
        }
    }
    file_put_contents($dir, $new_contents); // save the records to the file
    echo json_encode("Successfully updated record!");
}
else {
    echo json_encode("failed - user ID ". $id ." doesn't exist!");
}

Example:

input: "123,student"

Old file:

ID,occupation

123,student

124,brick layer

Running the code will change file to:

New file:

ID,occupation

124,brick layer

MAChitgarha
  • 3,728
  • 2
  • 33
  • 40
ChickenFeet
  • 2,653
  • 22
  • 26
  • Why have an `else` branch if the `if` branch does nothing? – mickmackusa May 01 '22 at 04:12
  • There is no reason, aside from readability perhaps. This was written 5 years ago while I was still a junior dev – ChickenFeet May 03 '22 at 05:40
  • Cool. When you have time, please update this post (that people are learning coding from) to reflect the best of your current dev knowledge. Also, modifying `$record` by reference seems unnecessary too. – mickmackusa May 03 '22 at 07:03
  • 1
    @mickmackusa I don't use PHP anymore, so I am not going to bother, as it would involve rewriting and testing. It is well commented so people learning can follow along. If people want a better answer they can use Naveed Ahmad's answer. – ChickenFeet May 04 '22 at 06:52
3

All answeres here have in common, that they load the complete file into the memory. Here is an implementation of removing one (or more) line(s) without coyping the files content into a variable.

The idea is to iterate over the files lines. If a line should be removed, the lines length is added to the $byte_offset. The next line is then moved $byte_offset bytes "upwards". This is done with all following lines. If all lines are processed, the files last $byte_offset bytes are removed.

I guess that this is faster for bigger files because nothing is copied. And I guess that at some file size the other answers do not work at all while this one should. But I didn't test it.

Usage:

$file = fopen("path/to/file", "a+");
// remove lines 1 and 2 and the line containing only "line"
fremove_line($file, 1, 2, "line");
fclose($file);

The code of the fremove_line() function:

/**
 * Remove the `$lines` by either their line number (as an int) or their content
 * (without trailing new-lines).
 * 
 * Example:
 * ```php
 * $file = fopen("path/to/file", "a+"); // must be opened writable
 * // remove lines 1 and 2 and the line containing only "line"
 * fremove_line($file, 1, 2, "line");
 * fclose($file);
 * ```
 * 
 * @param resource $file The file resource opened by `fopen()`
 * @param int|string ...$lines The one-based line number(s) or the full line 
 *     string(s) to remove, if the line does not exist, it is ignored
 * 
 * @return boolean True on success, false on failure
 */
function fremove_line($file, ..$lines): bool{
    // set the pointer to the start of the file
    if(!rewind($file)){
        return false;
    }

    // get the stat for the full size to truncate the file later on
    $stat = fstat($file);
    if(!$stat){
        return false;
    }

    $current_line = 1; // change to 0 for zero-based $lines
    $byte_offset = 0;
    while(($line = fgets($file)) !== false){
        // the bytes of the lines ("number of ASCII chars")
        $line_bytes = strlen($line);

        if($byte_offset > 0){
            // move lines upwards
            // go back the `$byte_offset`
            fseek($file, -1 * ($byte_offset + $line_bytes), SEEK_CUR);
            // move the line upwards, until the `$byte_offset` is reached
            if(!fwrite($file, $line)){
                return false;
            }
            // set the file pointer to the current line again, `fwrite()` added `$line_bytes`
            // already
            fseek($file, $byte_offset, SEEK_CUR);
        }

        // remove trailing line endings for comparing
        $line_content = preg_replace("~[\n\r]+$~", "", $line);

        if(in_array($current_line, $lines, true) || in_array($line_content, $lines, true)){
            // the `$current_line` should be removed so save to skip the number of bytes 
            $byte_offset += $line_bytes;
        }

        // keep track of the current line
        $current_line++;
    }

    // remove the end of the file
    return ftruncate($file, $stat["size"] - $byte_offset);
}
miile7
  • 2,547
  • 3
  • 23
  • 38
2

I think the best way to work with files is to act them like strings:

/**
 * Removes the first found line inside the given file.
 *
 * @param string $line The line content to be searched.
 * @param string $filePath Path of the file to be edited.
 * @param bool $removeOnlyFirstMatch Whether to remove only the first match or
 * the whole matches.
 * @return bool If any matches found (and removed) or not.
 *
 * @throw \RuntimeException If the file is empty.
 * @throw \RuntimeException When the file cannot be updated.
 */
function removeLineFromFile(
    string $line,
    string $filePath,
    bool $removeOnlyFirstMatch = true
): bool {
    // You can wrap it inside a try-catch block
    $file = new \SplFileObject($filePath, "r");

    // Checks whether the file size is not zero
    $fileSize = $file->getSize();
    if ($fileSize !== 0) {
        // Read the whole file 
        $fileContent = $file->fread($fileSize);
    } else {
        // File is empty
        throw new \RuntimeException("File '$filePath' is empty");
    }

    // Free file resources
    $file = null;

    // Divide file content into its lines
    $fileLineByLine = explode(PHP_EOL, $fileContent);

    $found = false;
    foreach ($fileLineByLine as $lineNumber => $thisLine) {
        if ($thisLine === $line) {
            $found = true;
            unset($fileLineByLine[$lineNumber]);

            if ($removeOnlyFirstMatch) {
                break;
            }
        }
    }

    // We don't need to update file either if the line not found
    if (!$found) {
        return false;
    }

    // Join lines together
    $newFileContent = implode(PHP_EOL, $fileLineByLine);

    // Finally, update the file
    $file = new \SplFileObject($filePath, "w");
    if ($file->fwrite($newFileContent) !== strlen($newFileContent)) {
        throw new \RuntimeException("Could not update the file '$filePath'");
    }

    return true;
}

Here is a brief description of what is being done: Get the whole file content, split the content into its lines (i.e. as an array), find the match(es) and remove them, join all lines together, and save the result back to the file (only if any changes happened).

Let's now use it:

// $dir is your filename, as you mentioned
removeLineFromFile($line, $dir);

Notes:

  • You can use fopen() family functions instead of SplFileObject, but I do recommend the object form, as it's exception-based, more robust and more efficient (in this case at least).

  • It's safe to unset() an element of an array being iterated using foreach (There's a comment here showing it can lead unexpected results, but it's totally wrong: As you can see in the example code, $value is copied (i.e. it's not a reference), and removing an array element does not affect it).

  • $line should not have new line characters like \n, otherwise, you may perform lots of redundant searches.

  • Don't use

    $fileLineByLine[$lineNumber] = "";
    // Or even
    $fileLineByLine[$lineNumber] = null;
    

    instead of

    unset($fileLineByLine[$key]);
    

    The reason is, the first case doesn't remove the line, it just clears the line (and an unwanted empty line will remain).

Hope it helps.

MAChitgarha
  • 3,728
  • 2
  • 33
  • 40
2

Convert text to array, remove first line and reconvert to text

$line=explode("\r\n",$text);
unset($line[0]);
$text=implode("\r\n",$line);
Vitalicus
  • 1,188
  • 13
  • 15
0

Like this:

file_put_contents($filename, str_replace($line . "\r\n", "", file_get_contents($filename)));