Arbeider med filsystemet i Elixir

Arbeide med filsystemet i Elixir skiller seg ikke fra å gjøre det med andre populære programmeringsspråk. Det er tre moduler for å løse denne oppgaven: IO, Fil, og Sti. De gir funksjoner for å åpne, opprette, endre, lese og ødelegge filer, utvide stier, etc. Det er imidlertid noen interessante gotchas som du bør være oppmerksom på.

I denne artikkelen vil vi snakke om å jobbe med filsystemet i Elixir mens du tar en titt på noen kodeeksempler.

Banemodulen

Banemodulen, som navnet antyder, brukes til å arbeide med filsystembaner. Funksjonene i denne modulen returnerer alltid UTF-8 kodede strenge.

For eksempel kan du utvide en bane og deretter generere en absolutt bane enkelt:

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

Merk, forresten, at i Windows, blir tilbakestrek erstattet med automatisk skråstreker automatisk. Den resulterende banen kan sendes til funksjonene til Fil modul, for eksempel:

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

Her bygger vi en full bane til filen og skriver deretter noe innhold til det.

Alt i alt jobber du med Sti Modulen er enkel, og de fleste funksjonene samhandler ikke med filsystemet. Vi vil se noen brukstilfeller for denne modulen senere i artikkelen.

IO og filmoduler

IO, som navnet tilsier, er modulen å arbeide med inngang og utgang. For eksempel gir den slike funksjoner som puts og undersøke. IO har et konsept av enheter, som kan være enten prosessidentifikatorer (PID) eller atomer. For eksempel er det : stdio og : stderr generiske enheter (som egentlig er snarveier). Enheter i Elixir opprettholder sin posisjon, slik at etterfølgende lese- eller skriveoperasjoner starter fra stedet der enheten tidligere ble benyttet.

Filmodulen gir oss igjen tilgang til filer som IO-enheter. Filer åpnes i binær modus som standard; Du kan imidlertid passere : utf8 som et alternativ. Også når et filnavn er oppgitt som en tegnliste ('Some_name.txt'), blir den alltid behandlet som UTF-8.

La oss nå se noen eksempler på bruk av modulene nevnt ovenfor.

Åpne og lese filer med IO

Den vanligste oppgaven er selvfølgelig å åpne og lese filer. For å åpne en fil kan en funksjon som kalles åpen / 2 brukes. Den aksepterer en bane til filen og en valgfri liste over moduser. For eksempel, la oss prøve å åpne en fil for lesing og skriving:

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

Du kan da lese denne filen ved hjelp av les / 2-funksjonen fra IO modul også:

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

Her leser vi fillinjen etter linje. Legg merke til : eof atom som betyr "slutten av filen".

Du kan også passere :alle i stedet for :linje å lese hele filen på en gang:

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

I dette tilfellet, : eof vil ikke bli returnert, i stedet får vi en tom streng. Hvorfor? Vel, fordi, som vi sa tidligere, vedlikeholder enhetene sin posisjon, og vi begynner å lese fra det tidligere besøkte stedet.

Det er også en åpen / 3-funksjon, som aksepterer en funksjon som det tredje argumentet. Etter at den bestått funksjonen er ferdig med arbeidet, blir filen lukket automatisk:

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

Lese filer med filmodul

I den forrige delen har jeg vist hvordan jeg bruker IO.read for å lese filer, men det ser ut til at Fil modulen har faktisk en funksjon med samme navn:

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

Denne funksjonen returnerer en tuple som inneholder resultatet av operasjonen og en binær dataobjekt. I dette eksemplet inneholder det "test", som er innholdet i filen.

Hvis operasjonen var mislykket, vil tuplen inneholde en :feil atom og feilens grunn:

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

Her, : enoent betyr at filen ikke eksisterer. Det er noen andre grunner som : eacces (har ingen tillatelser).

Den returnerte tuplen kan brukes i mønstermatching for å håndtere ulike utfall:

tilfelle File.read ("test.txt") gjør : ok, body -> IO.puts (body) : error, reason -> IO.puts ("Det oppstod en feil: # reason") slutt

I dette eksemplet skriver vi enten ut innholdet i filen eller viser en feilårsak.

En annen funksjon for å lese filer kalles les! / 1. Hvis du har kommet fra Ruby verden, har du sikkert gjettet hva det gjør. I utgangspunktet åpner denne funksjonen en fil og returnerer innholdet i form av en streng (ikke tuple!):

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

Men hvis noe går galt og filen ikke kan leses, oppstår en feil i stedet:

File.read! ("Non_existent.txt") # => (File.Error) kunne ikke lese filen "non_existent.txt": ingen slik fil eller katalog

Så for å være på den sikre siden kan du for eksempel bruke den eksisterende? / 1-funksjonen til å kontrollere om en fil egentlig eksisterer: 

defmodule Eksempel gjør def read_file (fil) hvis File.exists? (fil) gjør File.read! (fil) |> IO.inspect ende ende ende Eksempel.read_file ("non_existent.txt")

Flott, nå vet vi hvordan du leser filer. Men det er mye mer vi kan gjøre, så la oss fortsette til neste del!

Skriver til filer

For å skrive noe til en fil, bruk skrive / 3-funksjonen. Den aksepterer en bane til en fil, innholdet og en valgfri liste over moduser. Hvis filen ikke eksisterer, blir den opprettet automatisk. Hvis det imidlertid eksisterer, blir alt innholdet overskrevet som standard. For å unngå at dette skjer, sett inn : føyer modus:

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

I dette tilfellet vil innholdet bli lagt til filen og : ok vil bli returnert som et resultat. Hvis noe går galt, får du en tuple : feil, grunn, akkurat som med lese funksjon.

Også, det er en skriv! funksjon som gjør stort sett det samme, men gir et unntak hvis innholdet ikke kan skrives. For eksempel kan vi skrive et Elixir-program som lager et Ruby-program som i sin tur skriver ut "hei!":

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

Streaming filer

Filene kan faktisk være ganske store, og når du bruker lese funksjonen du laster inn alt innholdet i minnet. Den gode nyheten er at filer kan streames ganske enkelt:

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

I dette eksempelet åpner vi en fil, streker den linje for linje, og inspiserer hver linje. Resultatet ser slik ut:

"test \ n" "linje 2 \ n" "linje 3 \ n" "noen annen linje ... \ n"

Vær oppmerksom på at de nye linjesymbolene ikke fjernes automatisk, så du vil kanskje bli kvitt dem ved hjelp av String.replace / 4-funksjonen.

Det er litt kjedelig å streame en fillinje etter linje som vist i forrige eksempel. I stedet kan du stole på stream! / 3-funksjonen, som aksepterer en bane til filen og to valgfrie argumenter: en liste over moduser og en verdi som forklarer hvordan en fil skal leses (standardverdien er :linje):

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

I dette stykke kode streker vi en fil mens du fjerner newline-tegn og skriver ut hver linje. File.stream! er tregere enn File.read, men vi trenger ikke å vente til alle linjer er tilgjengelige - vi kan begynne å behandle innholdet med en gang. Dette er spesielt nyttig når du må lese en fil fra en ekstern plassering.

La oss ta en titt på et litt mer komplekst eksempel. Jeg vil gjerne streame en fil med mitt Elixir-skript, fjerne newline-tegn, og vise hver linje med et linjenummer ved siden av det:

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

Stream.with_index / 2 aksepterer en tallrik og returnerer en samling av tuples, hvor hver tuple inneholder en verdi og dens indeks. Deretter slår vi bare over denne samlingen og skriver ut linjenummeret og linjen selv. Som et resultat vil du se samme kode med linjenumre:

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

Flytte og fjerne filer

La oss også kortfattet dekke hvordan du kan manipulere filer - spesielt, flytt og fjern dem. Funksjonene vi er interessert i, er omdøpe / 2 og rm / 1. Jeg vil ikke bore deg ved å beskrive alle argumentene de aksepterer som du kan lese dokumentasjonen selv, og det er absolutt ingenting komplisert om dem. I stedet la vi se på noen eksempler.

Først vil jeg kode en funksjon som tar alle filer fra gjeldende katalog basert på en tilstand og deretter flytter dem til en annen katalog. Funksjonen skal kalles slik:

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

Så, her vil jeg gripe alt .tekst filer og flytte dem til tekster katalogen. Hvordan kan vi løse denne oppgaven? Vel, for det første, la oss definere en modul og en privat funksjon for å lage en destinasjonskatalog:

defmodule Copycat gjør def transfer_to (dir, fun) gjør prepare_dir! dir end defp prepare_dir! (dir) gjør med mindre File.exists? (dir) gjør File.mkdir! (dir) slutten enden

mkdir!, som du allerede har gjettet, prøver å lage en katalog og returnerer en feil hvis denne operasjonen mislykkes.

Deretter må vi ta alle filene fra gjeldende katalog. Dette kan gjøres ved hjelp av ls! funksjon, som returnerer en liste over filnavn:

File.ls!

Til slutt må vi filtrere den resulterende listen basert på den angitte funksjonen og omdøpe hver fil, som effektivt betyr å flytte den til en annen katalog. Her er den endelige versjonen av programmet:

defmodule Copycat gjør def transfer_to (dir, fun) gjør prepare_dir! (dir) File.ls! |> Stream.filter (& (fun. (& 1))) |> Enum.each (& (File.rename (& 1, "# dir / # & 1"))) avslutte defp prepare_dir! gjør med mindre File.exists? (dir) gjør File.mkdir! (dir) slutten slutten

La oss nå se rm i aksjon ved å kode en lignende funksjon som skal fjerne alle filer basert på en tilstand. Funksjonen vil bli kalt på følgende måte:

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

Her er den tilsvarende løsningen:

defmodule Copycat gjør def remove_if (morsom) gjør File.ls! |> Stream.filter (& (morsom. (& 1))) |> Enum.each (& File.rm! / 1) endeend

rm! / 1 vil øke en feil hvis filen ikke kan fjernes. Som alltid har den en rm / 1 motpart som vil returnere en tuple med feilens grunn hvis noe går galt.

Du kan merke at remove_if og Overfør til funksjoner er svært like. Så hvorfor fjerner vi ikke kod duplisering som en øvelse? Jeg legger til enda en privat funksjon som tar alle filene, filtrerer dem basert på den angitte tilstanden, og bruker deretter en operasjon til dem:

defp filter_and_process_files (tilstand, drift) gjør File.ls! |> Stream.filter (& (betingelse. (& 1))) |> Enum.each (& (operasjon. (& 1))) ende

Bruk nå denne funksjonen:

defmodule Copycat gjør def transfer_to (dir, fun) gjør prepare_dir! (dir) filter_and_process_files (morsom, fn (fil) -> File.rename (fil, "# dir / # file") end) ende def remove_if moro) gjør filter_and_process_files (moro, fn (fil) -> Fil.rm! (fil) slutten) slutt # ... ende

Tredjepartsløsninger

Elixirs samfunn vokser, og fancy nye bibliotek som løser ulike oppgaver, kommer frem. Den fantastiske Elixir GitHub repo viser noen populære løsninger, og selvfølgelig er det en seksjon med biblioteker for å jobbe med filer og kataloger. Det er implementeringer for filopplasting, overvåking, filnavn sanitisering og mer.

For eksempel er det en interessant løsning kalt Librex for å konvertere dokumentene dine ved hjelp av LibreOffice. For å se det i aksjon, kan du opprette et nytt prosjekt:

$ mix ny omformer

Legg deretter til en ny avhengighet til mix.exs-filen:

 defp deps gjør [: librex, "~> 1.0"] slutt

Deretter løp:

$ mix gjør deps.get, deps.compile

Deretter kan du inkludere biblioteket og utføre konverteringer:

defmodule Converter gjør import Librex def convert_and_remove (dir) gjør konvertere "some_path / file.odt", "other_path / 1.pdf" endeend

For at dette skal fungere, kan LibreOffice kjørbar (soffice.exe) må være til stede i STI. Ellers må du angi en bane til denne filen som et tredje argument:

defmodule Converter gjør importere Librex def convert_and_remove (dir) gjør konvertere "some_path / file.odt", "other_path / 1.pdf", "path / soffice" -enden

Konklusjon

Det var alt for i dag! I denne artikkelen har vi sett IO, Fil og Sti moduler i aksjon og diskutert noen nyttige funksjoner som åpen, lese, skrive, og andre. 

Det er mange andre funksjoner tilgjengelig for bruk, så pass på å bla gjennom Elixirs dokumentasjon. Det er også en introduksjonsveiledning på den offisielle nettsiden til språket som også kan komme til nytte.

Jeg håper du likte denne artikkelen og nå føler meg litt mer trygg på å jobbe med filsystemet i Elixir. Takk for at du bodde hos meg, og til neste gang!