Arbetar med filsystemet i Elixir

Att arbeta med filsystemet i Elixir skiljer sig inte riktigt från att göra det med andra populära programmeringsspråk. Det finns tre moduler för att lösa denna uppgift: IO, Fil, och Väg. De ger funktioner att öppna, skapa, ändra, läsa och förstöra filer, expandera vägar etc. Det finns dock några intressanta gotchas som du borde vara medveten om.

I den här artikeln kommer vi att prata om att arbeta med filsystemet i Elixir medan du tittar på några kodexempel.

Banmodulen

Banmodulen, som namnet antyder, används för att arbeta med filsystemvägar. Funktionerna i denna modul returnerar alltid UTF-8 kodade strängar.

Du kan till exempel expandera en sökvägen och sedan enkelt skapa en absolut väg:

Path.expand ('./ text.txt') |> Path.absname # => "f: /elixir/text.txt"

Observera förresten att i Windows ersätts backslashes med framåt snedstreck automatiskt. Den resulterande vägen kan överföras till funktionerna hos Fil modul, till exempel:

Path.expand ('./ text.txt') |> Path.absname |> File.write ("nytt innehåll!", [: Skriv]) # =>: ok

Här bygger vi en hel sökväg till filen och skriver sedan några innehåll till den.

Sammantaget arbetar med Väg modulen är enkel och de flesta funktionerna samverkar inte med filsystemet. Vi kommer se några användarfall för denna modul senare i artikeln.

IO och filmoduler

IO, som namnet antyder, är modulen att arbeta med ingång och utgång. Det ger till exempel sådana funktioner som sätter och inspektera. IO har ett koncept av enheter, vilket kan vara antingen processidentifierare (PID) eller atomer. Till exempel finns det : stdio och : stderr generiska enheter (som egentligen är genvägar). Enheter i Elixir behåller sin position, så efterföljande läs- eller skrivoperationer startar från den plats där enheten tidigare var åtkomst.

Filmodulen tillåter oss i sin tur att komma åt filer som IO-enheter. Filerna öppnas i binärt läge som standard; Du kan dock passera : utf8 som ett alternativ. Även när ett filnamn är angivet som en teckenlista ('Some_name.txt'), det behandlas alltid som UTF-8.

Låt oss nu se några exempel på att använda de ovan nämnda modulerna.

Öppna och läsa filer med IO

Den vanligaste uppgiften är förstås att öppna och läsa filer. För att öppna en fil kan en funktion som kallas öppen / 2 användas. Den accepterar en sökväg till filen och en valfri lista med lägen. Låt oss till exempel försöka öppna en fil för läsning och skrivning:

: ok, file = File.open ("test.txt", [: read,: write]) fil |> IO.inspect # => #PID<0.72.0>

Du kan då läsa den här filen med läs / 2-funktionen från IO modul också:

: ok, file = File.open ("test.txt", [: read,: write]) IO.read (fil,: linje) |> IO.inspect # => "test" IO.read ,: linje) |> IO.inspect # =>: eof

Här läser vi fillinjen efter rad. Notera : eof atom som betyder "slutet på filen".

Du kan också passera :Allt istället för :linje att läsa hela filen på en gång:

: ok, file = File.open ("test.txt", [: read,: write]) IO.read (file,: all) |> IO.inspect # => "test" IO.read ,: alla) |> IO.inspect # => "" 

I detta fall, : eof kommer inte att returneras, istället får vi en tom sträng. Varför? Tja, för att, som vi sa tidigare, enheterna behåller sin position, och vi börjar läsa från den tidigare åtkomliga platsen.

Det finns också en öppen / 3-funktion, som accepterar en funktion som det tredje argumentet. När den avslutade funktionen har avslutat sitt arbete stängs filen automatiskt:

File.open "test.txt", [: read], fn (fil) -> IO.read (file,: all) |> IO.inspect end

Läser filer med filmodul

I det föregående avsnittet har jag visat hur man använder IO.read för att läsa filer, men det verkar som att Fil modulen har faktiskt en funktion med samma namn:

File.read "test.txt" # => : ok, "test"

Denna funktion returnerar en tupel som innehåller resultatet av operationen och ett binärt dataobjekt. I det här exemplet innehåller det "test", vilket är innehållet i filen.

Om operationen misslyckades, kommer tupeln att innehålla en :fel atom och felets orsak:

File.read ("non_existent.txt") # => : error,: enoent

Här, : enoent betyder att filen inte existerar. Det finns andra orsaker till : eacces (har inga behörigheter).

Den återvände tupeln kan användas i mönstermatchning för att hantera olika resultat:

fallet File.read ("test.txt") gör : ok, body -> IO.puts (body) : error, reason -> IO.puts ("Det fanns ett fel: # reason") slutet

I det här exemplet skriver vi antingen ut innehållet eller visar en fel anledning.

En annan funktion att läsa filer kallas läs! / 1. Om du har kommit från Ruby-världen har du nog gissat vad det gör. I grunden öppnar den här funktionen en fil och returnerar dess innehåll i form av en sträng (inte tupel!):

File.read! ("Test.txt") # => "test"

Om något går fel och filen inte kan läsas tas ett fel upp i stället:

File.read! ("Non_existent.txt") # => (File.Error) kunde inte läsa filen "non_existent.txt": ingen sådan fil eller katalog

Så, för att vara på den säkra sidan kan du till exempel använda den existerande? / 1-funktionen för att kontrollera om en fil faktiskt existerar: 

defodule Exempel gör def read_file (fil) om File.exists? (fil) gör File.read! (file) |> IO.inspect slutänden Exempel.read_file ("non_existent.txt")

Bra, nu vet vi hur man läser filer. Det finns dock mycket mer vi kan göra, så låt oss fortsätta till nästa avsnitt!

Skriva till filer

För att skriva något till en fil, använd skriv / 3-funktionen. Den accepterar en sökväg till en fil, innehållet och en valfri lista med lägen. Om filen inte existerar skapas den automatiskt. Om det emellertid existerar kommer alla dess innehåll att skrivas över som standard. För att förhindra att detta händer, ställa in :bifoga läge:

File.write ("new.txt", "update!", [: Append]) | IO.inspect # =>: ok

I det här fallet läggs innehållet till filen och :ok kommer att returneras som ett resultat. Om något går fel får du en tuppel : fel, orsak, precis som med läsa fungera.

Det finns också en skriv! funktion som gör detsamma, men ger upphov till ett undantag om innehållet inte kan skrivas. Till exempel kan vi skriva ett Elixir-program som skapar ett Ruby-program som i sin tur skriver "hej!":

File.write! ("Test.rb", "puts \" hej! \ "")

Strömmande filer

Filerna kan faktiskt vara ganska stora, och när du använder läsa funktionen laddar du allt innehåll i minnet. Den goda nyheten är att filer kan streamas ganska enkelt:

File.open! ("Test.txt") |> IO.stream (: line) |> Enum.each (& IO.inspect / 1)

I det här exemplet öppnar vi en fil, strömmer den linjen för rad och inspekterar varje rad. Resultatet kommer att se ut så här:

"test \ n" "rad 2 \ n" "rad 3 \ n" "någon annan rad ... \ n"

Observera att de nya linjesymbolerna inte tas bort automatiskt, så du kanske vill bli av med dem med String.replace / 4-funktionen.

Det är lite tråkigt att strömma en fillinje för rad som visas i föregående exempel. Istället kan du lita på stream! / 3-funktionen, som accepterar en sökväg till filen och två valfria argument: en lista med lägen och ett värde som förklarar hur en fil ska läsas (standardvärdet är :linje):

File.stream! ("Test.txt") |> Stream.map (& (String.replace (& 1, "\ n", ""))) |> Enum.each (& IO.inspect / 1)

I den här koden sparar vi en fil medan du tar bort nya karaktärer och sedan skriver ut varje rad. File.stream! är långsammare än File.read, men vi behöver inte vänta tills alla linjer är tillgängliga-vi kan börja bearbeta innehållet direkt. Detta är särskilt användbart när du behöver läsa en fil från en avlägsen plats.

Låt oss ta en titt på ett något mer komplext exempel. Jag skulle vilja strömma en fil med mitt Elixir-skript, ta bort nya karaktärer och visa varje rad med ett radnummer bredvid det:

File.stream! ("Test.exs") |> Stream.map (& (String.replace (& 1, "\ n", ""))) |> Stream.with_index |> Enum.each (fn , line_num) -> IO.puts "# line_num + 1 # contents" slutet)

Stream.with_index / 2 accepterar en talbar och returnerar en samling tuplar, där varje tupel innehåller ett värde och dess index. Därefter repeterar vi bara över denna samling och skriver ut linjenummer och linjen själv. Som ett resultat kommer du att se samma kod med radnummer:

1 File.stream! ("Test.exs") |> 2 Stream.map (& (String.replace (& 1, "\ n", ""))) |> 3 Stream.with_index |> 4 Enum.each fn (content, line_num) -> 5 IO.puts "# line_num + 1 # contents" 6 slutet)

Flytta och ta bort filer

Låt oss nu också kortfattat täcka hur man manipulerar filer - specifikt, flytta och ta bort dem. Funktionerna som vi är intresserade av är byt namn / 2 och rm / 1. Jag kommer inte att dra dig genom att beskriva alla argument som de accepterar, eftersom du själv kan läsa dokumentationen, och det finns inget komplicerat om dem. Låt oss ta en titt på några exempel.

Först vill jag koda en funktion som tar alla filer från den aktuella katalogen baserat på ett villkor och flyttar dem sedan till en annan katalog. Funktionen ska kallas så här:

Copycat.transfer_to "texts", fn (fil) -> Path.extname (file) == ".txt" slutet

Så, här vill jag fånga allt .Text filer och flytta dem till texter katalogen. Hur kan vi lösa den här uppgiften? Tja, för det första, låt oss definiera en modul och en privat funktion för att förbereda en målkatalog:

defmodule Copycat gör def transfer_to (dir, fun) do prepare_dir! dir end defp prepare_dir! (dir) gör om inte File.exists? (dir) gör File.mkdir! (dir) slutet änden

mkdir! Som du redan gissat försöker du skapa en katalog och returnerar ett fel om den här åtgärden misslyckas.

Därefter måste vi ta alla filer från den aktuella katalogen. Detta kan göras med hjälp av ls! funktion, som returnerar en lista med filnamn:

File.ls!

Slutligen måste vi filtrera den resulterande listan baserat på den angivna funktionen och byta namn på varje fil, vilket innebär att det ska flyttas till en annan katalog. Här är den slutliga versionen av programmet:

defmodule Copycat gör def transfer_to (dir, fun) do prepare_dir! (dir) File.ls! |> Stream.filter (& (fun. (& 1))) |> Enum.each (& (Filnamn (& 1, "# dir / # & 1"))) sluta defp prepare_dir! gör om inte File.exists? (dir) gör File.mkdir! (dir) slutet slutet

Nu får vi se rm i funktion genom att koda en liknande funktion som kommer att ta bort alla filer baserat på ett tillstånd. Funktionen kommer att ringas på följande sätt:

Copycat.remove_if fn (fil) -> Path.extname (file) == ".csv" slutet

Här är motsvarande lösning:

defmodule Copycat gör def remove_if (fun) do File.ls! |> Stream.filter (& (kul. (& 1))) |> Enum.each (& File.rm! / 1) änden

rm! / 1 ger upphov till ett fel om filen inte kan tas bort. Som alltid har den en rm / 1 motsvarighet som kommer att returnera en tupel med felets orsak om något går fel.

Du kanske noterar att remove_if och överföra till funktionerna är mycket lika. Så varför tar vi inte bort koddubbling som en övning? Jag lägger till ytterligare en privat funktion som tar alla filer, filtrerar dem baserat på det angivna villkoret och applicerar sedan en operation till dem:

defp filter_and_process_files (villkor, drift) gör File.ls! |> Stream.filter (& (villkor. (& 1))) |> Enum.each (& (operation. (& 1))) slut

Använd nu enkelt denna funktion:

def.dll Copycat gör def transfer_to (dir, fun) gör prepare_dir! (dir) filter_and_process_files (kul, fn (fil) -> Fil.rename (fil, "# dir / # file") slut) slutfel remove_if kul) gör filter_and_process_files (kul, fn (fil) -> Fil.rm! (fil) slut) slut # ... än

Tredjepartslösningar

Elixirs samhälle växer och nya bibliotek ser ut att lösa olika uppgifter uppkommer. Den enorma Elixir GitHub repo listar några populära lösningar, och det finns givetvis ett avsnitt med bibliotek för att arbeta med filer och kataloger. Det finns implementeringar för filuppladdning, övervakning, sanering av filnamn och mer.

Till exempel finns det en intressant lösning som heter Librex för att konvertera dina dokument med hjälp av LibreOffice. För att se det i åtgärd kan du skapa ett nytt projekt:

$ mix ny omvandlare

Lägg sedan till ett nytt beroende av mix.exs-filen:

 defp deps gör [: librex, "~> 1.0"] slutet

Därefter springa:

$ mix gör deps.get, deps.compile

Därefter kan du inkludera biblioteket och utföra omvandlingar:

defmodule Converter gör import Librex def convert_and_remove (dir) konvertera "some_path / file.odt", "other_path / 1.pdf" slutet

För att detta ska fungera kan LibreOffice-körbar (soffice.exe) måste vara närvarande i VÄG. Annars måste du ange en sökväg till den här filen som ett tredje argument:

defmodule Converter gör import Librex def convert_and_remove (dir) konvertera "some_path / file.odt", "other_path / 1.pdf", "path / soffice" -änden

Slutsats

Det är allt för idag! I den här artikeln har vi sett IO, Fil och Väg moduler i åtgärd och diskuterade några användbara funktioner som öppna, läsa, skriva, och andra. 

Det finns många andra funktioner tillgängliga för användning, så var noga med att bläddra igenom Elixirs dokumentation. Det finns också en introduktionshandledning på den officiella hemsidan för språket som också kan vara användbart.

Jag hoppas att du haft den här artikeln och känner nu lite mer säker på att arbeta med filsystemet i Elixir. Tack för att du bodde hos mig, och tills nästa gång!